Compare commits

...
56 Commits
Author SHA1 Message Date
Gani Georgiev f56adf26f4 added the app name in the document title and fixed the double initial load on records list 2022-07-18 19:44:10 +03:00
Gani Georgiev f8f3ca25ee updated duplicated field name error 2022-07-18 18:10:30 +03:00
Gani Georgiev e01f76d37b allow switching schema field names when renaming fields 2022-07-18 16:26:37 +03:00
Gani Georgiev 47fc9b1066 normalized null handling in search filters 2022-07-18 14:07:25 +03:00
Gani Georgiev eaf08a5c15 [#151] updated the tests to ensure that the cascaded record files are also deleted 2022-07-18 13:19:07 +03:00
Gani Georgiev 8ef3d4e966 [#151] remove files on cascade deletion 2022-07-18 12:04:27 +03:00
Gani Georgiev 04e0cec32c updated ui/dist 2022-07-18 01:06:55 +03:00
Gani Georgiev 9a231ba7b3 applied some of the changes from #149 2022-07-18 01:03:09 +03:00
Gani Georgiev 7f959011b8 moved field option btns inside the panel 2022-07-18 00:55:53 +03:00
Gani Georgiev 994761b728 normalized the caster to return always non-null value and fixed minor ui issues 2022-07-18 00:16:09 +03:00
Gani Georgiev f19b9e3552 commented golangci-lint action as it is not go1.18+ ready 2022-07-17 22:25:28 +03:00
Gani Georgiev 36783b8f04 added golangci-lint action 2022-07-17 22:20:40 +03:00
Gani Georgiev a076cc906f [#147] added CONTRIBUTING.md 2022-07-17 20:33:12 +03:00
Gani Georgiev b1a30f4050 fixed figure tag typo 2022-07-16 11:57:31 +03:00
Gani Georgiev 4506fb17e9 adding video presets to the file options 2022-07-16 11:54:54 +03:00
Gani Georgiev 72f72bc84f added invalid form-group styles 2022-07-16 10:40:04 +03:00
Gani Georgiev 7fd5102fb5 updated EmailOptions domain input tooltips 2022-07-16 10:39:19 +03:00
Gani Georgiev d6bdc51009 changed the storage slide panel transition to local 2022-07-16 10:38:47 +03:00
Gani Georgiev 789373d15d [#100] resets the initial loadList results 2022-07-15 19:37:30 +03:00
Gani Georgiev 2dc000da65 improve error reporting on OAuth2 user profile fetch 2022-07-15 18:52:37 +03:00
Simon Krauter 1095637bcd [#116] fix BaseModel.RefreshUpdated comment 2022-07-14 23:17:53 +03:00
Gani Georgiev c4fcba5210 [#109] prealocated handlers and replaced OnRecordBeforeDeleteRequest with OnModelBeforeDelete 2022-07-14 22:35:57 +03:00
Gani Georgiev d8c8289269 added odd default circle icon size for better visual vertical alignment in firefox 2022-07-14 21:21:47 +03:00
Gani Georgiev dbbfa243bc added new lines for readability and consistency 2022-07-14 20:01:53 +03:00
Valley a16b0c9004 [#114] simplified some code by returning early and added cap for slices 2022-07-14 19:26:08 +03:00
Gani Georgiev 03a7bafa66 use the original vite2 default port and bump the min js-sdk to v0.2.1 2022-07-14 17:09:08 +03:00
Gani Georgiev d129959098 added store.RemoveAll() helper method 2022-07-14 16:39:42 +03:00
Gani Georgiev 6749559a22 log the response error not the handler one 2022-07-14 11:52:35 +03:00
Gani Georgiev 28bc2678e9 minor ui improvements and upgraded dependencies 2022-07-14 09:58:53 +03:00
Gani Georgiev d4202e696b [#99] fix AutoExpandTextArea scrolling 2022-07-13 22:44:59 +03:00
Gani Georgiev 9de3cc99a0 updated api preview body params col sizes 2022-07-13 08:48:38 +03:00
Franco Profeti 111bc59472 [#93] improved the README for no golang devs 2022-07-13 08:12:25 +03:00
Gani Georgiev b2647ebca9 synced refresh button js timeout with the css animation 2022-07-12 20:03:13 +03:00
Gani Georgiev ef226cf9c1 added refresh button to the other listing pages 2022-07-12 19:56:22 +03:00
Valley 63d5a8d633 [#89] simplified some code by returning early and reducing local variable scopes 2022-07-12 19:52:09 +03:00
Gani Georgiev d71c3cd19c updated ui/dist 2022-07-12 18:11:50 +03:00
Cornelius Müller 240bd6790a [#83] added refresh button to page records view 2022-07-12 18:08:57 +03:00
Gani Georgiev 05a4071eba [#80] fixed before hooks data and added optional interceptor to upsert submit 2022-07-12 13:42:06 +03:00
Valley ce857985be [#82] removed version cmd and make use of cobra.Version 2022-07-12 07:57:36 +03:00
Gani Georgiev 320d1482a4 [#77] add warning on storage type change 2022-07-11 22:23:22 +03:00
Gani Georgiev 46399dddac [#78] enable fully qualified URIs for S3 endpoints and improved error reporting when uploading or deleting files 2022-07-11 21:00:17 +03:00
Gani Georgiev 52c288d9db added linter skip comments and removed the Presentator specific inflector.Usernamify 2022-07-11 16:16:01 +03:00
Gani Georgiev ed741662b2 removed the v prefix from the version command 2022-07-11 10:30:34 +03:00
Gani Georgiev 0c14f32822 added additional info about the @expand query parameter 2022-07-11 10:30:20 +03:00
毛亚琛 ec0d3b0d3d [#69] automatically add version information 2022-07-11 10:26:55 +03:00
wenqingl 10d7faea31 [#62] fixed typo in README
defintions -> definitions
2022-07-11 07:36:38 +03:00
Gani Georgiev f62664098e updated ui/dist 2022-07-10 21:14:38 +03:00
毛亚琛 1dfc314bc1 [#60] fix list api docs example 2022-07-10 21:07:04 +03:00
Gani Georgiev 7b2d88fa30 [#45] don't set Last-Modified header if time.location data cannot be loaded 2022-07-10 20:53:24 +03:00
egor-romanov 72cb2d3f43 [#53] fix migrations register in framework mode 2022-07-10 14:40:51 +03:00
Gani Georgiev 0739e90ff2 [#31] replaced the initial admin create interactive cli with Installer web page 2022-07-10 11:46:21 +03:00
Valley 460c684caa [#47] fixed some doc and code inconsistencies and removed some redundant parentheses 2022-07-10 09:13:44 +03:00
Valley d64fbf9011 [#38] added lint and used the lint suggestions 2022-07-09 17:17:41 +03:00
Gani Georgiev dfd9528847 [#32] changed step value to any 2022-07-09 11:16:35 +03:00
Gani Georgiev d4d425c61d [#6] added note for go1.19 on windows 2022-07-09 11:05:42 +03:00
Gani Georgiev 0f0c00bd03 [#32] allow decimals in the number field 2022-07-09 10:57:21 +03:00
158 changed files with 2780 additions and 2522 deletions

No files matched your search

+5
View File
@@ -29,6 +29,11 @@ jobs:
- name: Build Admin dashboard UI
run: npm --prefix=./ui ci && npm --prefix=./ui run build
# 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 GoReleaser
uses: goreleaser/goreleaser-action@v3
with:
+1
View File
@@ -1,4 +1,5 @@
/.vscode/
.idea
.DS_Store
+2
View File
@@ -9,6 +9,8 @@ before:
builds:
- main: ./examples/base
binary: pocketbase
ldflags:
- -s -w -X github.com/pocketbase/pocketbase.Version={{ .Version }}
env:
- CGO_ENABLED=0
goos:
+80
View File
@@ -0,0 +1,80 @@
Contributing to PocketBase
======================================================================
Thanks for taking the time to improve PocketBase!
This document describes how to prepare a PR for a change in the main repository.
- [Prerequisites](#prerequisites)
- [Making changes in the Go code](#making-changes-in-the-go-code)
- [Making changes in the Admin UI](#making-changes-in-the-admin-ui)
## Prerequisites
- Go 1.18+ (for making changes in the Go code)
- Node 16+ (for making changes in the Admin UI)
If you haven't already, you can fork the main repository and clone your fork so that you can work locally:
```
git clone https://github.com/your_username/pocketbase.git
```
> It is recommended to create a new branch from master for each of your bugfixes and features.
> This is required if you are planning to submit multiple PRs in order to keep the changes separate for review until they eventually get merged.
## Making changes in the Go code
PocketBase is a Go package, which means that in order to run the project you'll have to create a Go `main` program that imports the package.
The repository already includes such program, located in `/examples/base`, that is also used for the prebuilt executables.
So, let's assume that you already done some changes in the PocketBase Go code and you want now to run them:
1. Navigate to `examples/base`
2. Run `go run main.go serve`
This will start a web server on `http://localhost:8090` with the embedded prebuilt Admin UI from `ui/dist`. And that's it!
**Before making a PR to the main repository, it is a good idea to:**
- Add unit/integration tests for your changes (we are using the standard `testing` go package).
To run the tests, you could execute (while in the root project directory):
```sh
go test ./...
# or using the Makefile
make test
```
- Run the linter - **golangci** ([see how to install](https://golangci-lint.run/usage/install/#local-installation)):
```sh
golangci-lint run -c ./golangci.yml ./...
# or using the Makefile
make lint
```
## Making changes in the Admin UI
PocketBase Admin UI is a single-page application (SPA) built with Svelte and Vite.
To start the Admin UI:
1. Navigate to the `ui` project directory
2. Run `npm install` to install the node dependencies
3. Start vite's dev server
```sh
npm run dev
```
You could open the browser and access the running Admin UI at `http://localhost:3000`.
Since the Admin UI is just a client-side application, you need to have the PocketBase backend server also running in the background (either manually running the `examples/base/main.go` or download a prebuilt executable).
> By default, the Admin UI is expecting the backend server to be started at `http://localhost:8090`, but you could change that by creating a new `ui/.env.development.local` file with `PB_BACKEND_URL = YOUR_ADDRESS` variable inside it.
Every change you make in the Admin UI should be automatically reflected in the browser at `http://localhost:3000` without reloading the page.
Once you are done with your changes, you have to build the Admin UI with `npm run build`, so that it can be embedded in the go package. And that's it - you can make your PR to the main PocketBase repository.
+9
View File
@@ -0,0 +1,9 @@
lint:
golangci-lint run -c ./golangci.yml ./...
test:
go test -v --cover ./...
test-report:
go test -v --cover -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
+8 -4
View File
@@ -19,7 +19,7 @@
**For documentation and examples, please visit https://pocketbase.io/docs.**
> ⚠️ Although the web API defintions are considered stable,
> ⚠️ Although the web API definitions are considered stable,
> please keep in mind that PocketBase is still under active development
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
@@ -43,6 +43,7 @@ your own custom app specific business logic and still have a single portable exe
# go 1.18+
go get github.com/pocketbase/pocketbase
```
> For Windows, you may have to use go 1.19+ due to an incorrect js mime type in the Windows Registry (see [issue#6](https://github.com/pocketbase/pocketbase/issues/6)).
### Example
@@ -90,10 +91,12 @@ Running/building the application is the same as for any other Go program, aka. j
**PocketBase embeds SQLite, but doesn't require CGO.**
If CGO is enabled, it will use [mattn/go-sqlite3](https://pkg.go.dev/github.com/mattn/go-sqlite3) driver, otherwise - [modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite).
If CGO is enabled, it will use [mattn/go-sqlite3](https://pkg.go.dev/github.com/mattn/go-sqlite3) driver, otherwise - [modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite). You can enable and disable CGO by setting the `CGO_ENABLED` enviroment variable to `1` or `0` respectively.
Enable CGO only if you really need to squeeze the read/write query performance at the expense of complicating cross compilation.
To build the minimal standalone executable, like the prebuilt ones in the releases page, you can simply run `go build` inside the `examples/base` directory.
### Testing
PocketBase comes with mixed bag of unit and integration tests.
@@ -117,8 +120,9 @@ PocketBase is free and open source project licensed under the [MIT License](LICE
You could help continuing its development by:
- [Suggest new features, report issues and fix bugs](https://github.com/pocketbase/pocketbase/issues)
- [Contribute to the source code](CONTRIBUTING.md)
- [Suggest new features and report issues](https://github.com/pocketbase/pocketbase/issues)
- [Donate a small amount](https://pocketbase.io/support-us)
> Please also note that PocketBase was initially created to serve as a new backend for my other open source project - [Presentator](https://presentator.io) (see [#183](https://github.com/presentator/presentator/issues/183)),
> Please note that PocketBase was initially created to serve as a new backend for my other open source project - [Presentator](https://presentator.io) (see [#183](https://github.com/presentator/presentator/issues/183)),
so all feature requests will be first aligned with what we need for Presentator v3.
+31 -23
View File
@@ -24,7 +24,7 @@ func BindAdminApi(app core.App, rg *echo.Group) {
subGroup.POST("/confirm-password-reset", api.confirmPasswordReset)
subGroup.POST("/refresh", api.refresh, RequireAdminAuth())
subGroup.GET("", api.list, RequireAdminAuth())
subGroup.POST("", api.create, 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())
@@ -66,7 +66,7 @@ func (api *adminApi) refresh(c echo.Context) error {
func (api *adminApi) emailAuth(c echo.Context) error {
form := forms.NewAdminLogin(api.app)
if readErr := c.Bind(form); readErr != nil {
return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr)
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
}
admin, submitErr := form.Submit()
@@ -80,11 +80,11 @@ func (api *adminApi) emailAuth(c echo.Context) error {
func (api *adminApi) requestPasswordReset(c echo.Context) error {
form := forms.NewAdminPasswordResetRequest(api.app)
if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("An error occured while reading the submitted data.", err)
return rest.NewBadRequestError("An error occurred while reading the submitted data.", err)
}
if err := form.Validate(); err != nil {
return rest.NewBadRequestError("An error occured while validating the form.", err)
return rest.NewBadRequestError("An error occurred while validating the form.", err)
}
// run in background because we don't need to show the result
@@ -101,7 +101,7 @@ func (api *adminApi) requestPasswordReset(c echo.Context) error {
func (api *adminApi) confirmPasswordReset(c echo.Context) error {
form := forms.NewAdminPasswordResetConfirm(api.app)
if readErr := c.Bind(form); readErr != nil {
return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr)
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
}
admin, submitErr := form.Submit()
@@ -166,7 +166,7 @@ func (api *adminApi) create(c echo.Context) error {
// load request
if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
}
event := &core.AdminCreateEvent{
@@ -174,20 +174,24 @@ func (api *adminApi) create(c echo.Context) error {
Admin: admin,
}
handlerErr := api.app.OnAdminBeforeCreateRequest().Trigger(event, func(e *core.AdminCreateEvent) error {
// create the admin
if err := form.Submit(); err != nil {
return rest.NewBadRequestError("Failed to create admin.", err)
}
// create the admin
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
return api.app.OnAdminBeforeCreateRequest().Trigger(event, func(e *core.AdminCreateEvent) error {
if err := next(); err != nil {
return rest.NewBadRequestError("Failed to create admin.", err)
}
return e.HttpContext.JSON(http.StatusOK, e.Admin)
return e.HttpContext.JSON(http.StatusOK, e.Admin)
})
}
})
if handlerErr == nil {
if submitErr == nil {
api.app.OnAdminAfterCreateRequest().Trigger(event)
}
return handlerErr
return submitErr
}
func (api *adminApi) update(c echo.Context) error {
@@ -205,7 +209,7 @@ func (api *adminApi) update(c echo.Context) error {
// load request
if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
}
event := &core.AdminUpdateEvent{
@@ -213,20 +217,24 @@ func (api *adminApi) update(c echo.Context) error {
Admin: admin,
}
handlerErr := api.app.OnAdminBeforeUpdateRequest().Trigger(event, func(e *core.AdminUpdateEvent) error {
// update the admin
if err := form.Submit(); err != nil {
return rest.NewBadRequestError("Failed to update admin.", err)
}
// update the admin
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
return api.app.OnAdminBeforeUpdateRequest().Trigger(event, func(e *core.AdminUpdateEvent) error {
if err := next(); err != nil {
return rest.NewBadRequestError("Failed to update admin.", err)
}
return e.HttpContext.JSON(http.StatusOK, e.Admin)
return e.HttpContext.JSON(http.StatusOK, e.Admin)
})
}
})
if handlerErr == nil {
if submitErr == nil {
api.app.OnAdminAfterUpdateRequest().Trigger(event)
}
return handlerErr
return submitErr
}
func (api *adminApi) delete(c echo.Context) error {
+26 -10
View File
@@ -456,12 +456,37 @@ func TestAdminDelete(t *testing.T) {
func TestAdminCreate(t *testing.T) {
scenarios := []tests.ApiScenario{
{
Name: "unauthorized",
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}`),
BeforeFunc: 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,
@@ -482,9 +507,6 @@ func TestAdminCreate(t *testing.T) {
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."},"password":{"code":"validation_required","message":"Cannot be blank."}}`},
ExpectedEvents: map[string]int{
"OnAdminBeforeCreateRequest": 1,
},
},
{
Name: "authorized as admin + invalid data format",
@@ -507,9 +529,6 @@ func TestAdminCreate(t *testing.T) {
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{"avatar":{"code":"validation_max_less_equal_than_required","message":"Must be no greater than 9."},"email":{"code":"validation_admin_email_exists","message":"Admin email already exists."},"password":{"code":"validation_length_out_of_range","message":"The length must be between 10 and 100."},"passwordConfirm":{"code":"validation_values_mismatch","message":"Values don't match."}}`},
ExpectedEvents: map[string]int{
"OnAdminBeforeCreateRequest": 1,
},
},
{
Name: "authorized as admin + valid data",
@@ -622,9 +641,6 @@ func TestAdminUpdate(t *testing.T) {
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{"avatar":{"code":"validation_max_less_equal_than_required","message":"Must be no greater than 9."},"email":{"code":"validation_admin_email_exists","message":"Admin email already exists."},"password":{"code":"validation_length_out_of_range","message":"The length must be between 10 and 100."},"passwordConfirm":{"code":"validation_values_mismatch","message":"Values don't match."}}`},
ExpectedEvents: map[string]int{
"OnAdminBeforeUpdateRequest": 1,
},
},
{
Method: http.MethodPatch,
+81 -10
View File
@@ -15,6 +15,7 @@ import (
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/rest"
"github.com/pocketbase/pocketbase/ui"
"github.com/spf13/cast"
)
// InitApi creates a configured echo instance with registered
@@ -38,13 +39,13 @@ func InitApi(app core.App) (*echo.Echo, error) {
var apiErr *rest.ApiError
switch v := err.(type) {
case (*echo.HTTPError):
case *echo.HTTPError:
if v.Internal != nil && app.IsDebug() {
log.Println(v.Internal)
}
msg := fmt.Sprintf("%v", v.Message)
apiErr = rest.NewApiError(v.Code, msg, v)
case (*rest.ApiError):
case *rest.ApiError:
if app.IsDebug() && v.RawData() != nil {
log.Println(v.RawData())
}
@@ -67,17 +68,12 @@ func InitApi(app core.App) (*echo.Echo, error) {
// truly rare case; eg. client already disconnected
if cErr != nil && app.IsDebug() {
log.Println(err)
log.Println(cErr)
}
}
// serves /ui/dist/index.html file
// (explicit route is used to avoid conflicts with `RemoveTrailingSlash` middleware)
e.FileFS("/_", "index.html", ui.DistIndexHTML, middleware.Gzip())
// serves static files from the /ui/dist directory
// (similar to echo.StaticFS but with gzip middleware enabled)
e.GET("/_/*", StaticDirectoryHandler(ui.DistDirFS, false), middleware.Gzip())
// admin ui routes
bindStaticAdminUI(app, e)
// default routes
api := e.Group("/api")
@@ -129,3 +125,78 @@ func StaticDirectoryHandler(fileSystem fs.FS, disablePathUnescaping bool) echo.H
return c.FileFS(name, fileSystem)
}
}
// bindStaticAdminUI registers the endpoints that serves the static admin UI.
func bindStaticAdminUI(app core.App, e *echo.Echo) error {
// serves /ui/dist/index.html file
// (explicit route is used to avoid conflicts with `RemoveTrailingSlash` middleware)
e.FileFS(
"/_",
"index.html",
ui.DistIndexHTML,
middleware.Gzip(),
installerRedirect(app),
)
// serves static files from the /ui/dist directory
// (similar to echo.StaticFS but with gzip middleware enabled)
e.GET(
"/_/*",
StaticDirectoryHandler(ui.DistDirFS, false),
middleware.Gzip(),
)
return nil
}
const totalAdminsCacheKey = "totalAdmins"
func updateTotalAdminsCache(app core.App) error {
total, err := app.Dao().TotalAdmins()
if err != nil {
return err
}
app.Cache().Set(totalAdminsCacheKey, total)
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 totalAdminsCacheKey value up-to-date
app.OnAdminAfterCreateRequest().Add(func(data *core.AdminCreateEvent) error {
return updateTotalAdminsCache(app)
})
app.OnAdminAfterDeleteRequest().Add(func(data *core.AdminDeleteEvent) error {
return updateTotalAdminsCache(app)
})
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// load into cache (if not already)
if !app.Cache().Has(totalAdminsCacheKey) {
if err := updateTotalAdminsCache(app); err != nil {
return err
}
}
totalAdmins := cast.ToInt(app.Cache().Get(totalAdminsCacheKey))
_, hasInstallerParam := c.Request().URL.Query()["installer"]
if totalAdmins == 0 && !hasInstallerParam {
// redirect to the installer page
return c.Redirect(http.StatusTemporaryRedirect, "/_/?installer#")
}
if totalAdmins != 0 && hasInstallerParam {
// redirect to the home page
return c.Redirect(http.StatusTemporaryRedirect, "/_/#/")
}
return next(c)
}
}
}
+28 -44
View File
@@ -1,8 +1,6 @@
package apis
import (
"errors"
"log"
"net/http"
"github.com/labstack/echo/v5"
@@ -76,9 +74,9 @@ func (api *collectionApi) create(c echo.Context) error {
form := forms.NewCollectionUpsert(api.app, collection)
// read
// load request
if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
}
event := &core.CollectionCreateEvent{
@@ -86,20 +84,24 @@ func (api *collectionApi) create(c echo.Context) error {
Collection: collection,
}
handlerErr := api.app.OnCollectionBeforeCreateRequest().Trigger(event, func(e *core.CollectionCreateEvent) error {
// submit
if err := form.Submit(); err != nil {
return rest.NewBadRequestError("Failed to create the collection.", err)
}
// create the collection
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
return api.app.OnCollectionBeforeCreateRequest().Trigger(event, func(e *core.CollectionCreateEvent) error {
if err := next(); err != nil {
return rest.NewBadRequestError("Failed to create the collection.", err)
}
return e.HttpContext.JSON(http.StatusOK, e.Collection)
return e.HttpContext.JSON(http.StatusOK, e.Collection)
})
}
})
if handlerErr == nil {
if submitErr == nil {
api.app.OnCollectionAfterCreateRequest().Trigger(event)
}
return handlerErr
return submitErr
}
func (api *collectionApi) update(c echo.Context) error {
@@ -110,9 +112,9 @@ func (api *collectionApi) update(c echo.Context) error {
form := forms.NewCollectionUpsert(api.app, collection)
// read
// load request
if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
}
event := &core.CollectionUpdateEvent{
@@ -120,20 +122,24 @@ func (api *collectionApi) update(c echo.Context) error {
Collection: collection,
}
handlerErr := api.app.OnCollectionBeforeUpdateRequest().Trigger(event, func(e *core.CollectionUpdateEvent) error {
// submit
if err := form.Submit(); err != nil {
return rest.NewBadRequestError("Failed to update the collection.", err)
}
// update the collection
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
return api.app.OnCollectionBeforeUpdateRequest().Trigger(event, func(e *core.CollectionUpdateEvent) error {
if err := next(); err != nil {
return rest.NewBadRequestError("Failed to update the collection.", err)
}
return e.HttpContext.JSON(http.StatusOK, e.Collection)
return e.HttpContext.JSON(http.StatusOK, e.Collection)
})
}
})
if handlerErr == nil {
if submitErr == nil {
api.app.OnCollectionAfterUpdateRequest().Trigger(event)
}
return handlerErr
return submitErr
}
func (api *collectionApi) delete(c echo.Context) error {
@@ -152,13 +158,6 @@ func (api *collectionApi) delete(c echo.Context) error {
return rest.NewBadRequestError("Failed to delete collection. Make sure that the collection is not referenced by other collections.", err)
}
// try to delete the collection files
if err := api.deleteCollectionFiles(e.Collection); err != nil && api.app.IsDebug() {
// non critical error - only log for debug
// (usually could happen because of S3 api limits)
log.Println(err)
}
return e.HttpContext.NoContent(http.StatusNoContent)
})
@@ -168,18 +167,3 @@ func (api *collectionApi) delete(c echo.Context) error {
return handlerErr
}
func (api *collectionApi) deleteCollectionFiles(collection *models.Collection) error {
fs, err := api.app.NewFilesystem()
if err != nil {
return err
}
defer fs.Close()
failed := fs.DeletePrefix(collection.BaseFilesPath())
if len(failed) > 0 {
return errors.New("Failed to delete all record files.")
}
return nil
}
-9
View File
@@ -297,9 +297,6 @@ func TestCollectionCreate(t *testing.T) {
`"name":{"code":"validation_required"`,
`"schema":{"code":"validation_required"`,
},
ExpectedEvents: map[string]int{
"OnCollectionBeforeCreateRequest": 1,
},
},
{
Name: "authorized as admin + invalid data (eg. existing name)",
@@ -315,9 +312,6 @@ func TestCollectionCreate(t *testing.T) {
`"name":{"code":"validation_collection_name_exists"`,
`"schema":{"0":{"name":{"code":"validation_required"`,
},
ExpectedEvents: map[string]int{
"OnCollectionBeforeCreateRequest": 1,
},
},
{
Name: "authorized as admin + valid data",
@@ -399,9 +393,6 @@ func TestCollectionUpdate(t *testing.T) {
`"data":{`,
`"name":{"code":"validation_collection_name_exists"`,
},
ExpectedEvents: map[string]int{
"OnCollectionBeforeUpdateRequest": 1,
},
},
{
Name: "authorized as admin + valid data",
+2 -1
View File
@@ -1,13 +1,14 @@
package apis_test
import (
"github.com/pocketbase/pocketbase/tests"
"net/http"
"os"
"path"
"path/filepath"
"runtime"
"testing"
"github.com/pocketbase/pocketbase/tests"
)
func TestFileDownload(t *testing.T) {
+1 -1
View File
@@ -3,8 +3,8 @@ package apis
import (
"net/http"
"github.com/pocketbase/dbx"
"github.com/labstack/echo/v5"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tools/rest"
+25 -3
View File
@@ -77,6 +77,28 @@ func RequireAdminAuth() echo.MiddlewareFunc {
}
}
// RequireAdminAuthOnlyIfAny middleware requires a request to have
// a valid admin Authorization header set (aka. `Authorization: Admin ...`)
// 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 {
totalAdmins, err := app.Dao().TotalAdmins()
if err != nil {
return rest.NewBadRequestError("Failed to fetch admins info.", err)
}
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
if admin != nil || totalAdmins == 0 {
return next(c)
}
return rest.NewUnauthorizedError("The request requires admin authorization token to be set.", nil)
}
}
}
// RequireAdminOrUserAuth middleware requires a request to have
// a valid admin or user Authorization header set
// (aka. `Authorization: Admin ...` or `Authorization: User ...`).
@@ -204,11 +226,11 @@ func ActivityLogger(app core.App) echo.MiddlewareFunc {
if err != nil {
switch v := err.(type) {
case (*echo.HTTPError):
case *echo.HTTPError:
status = v.Code
meta["errorMessage"] = v.Message
meta["errorDetails"] = fmt.Sprint(v.Internal)
case (*rest.ApiError):
case *rest.ApiError:
status = v.Code
meta["errorMessage"] = v.Message
meta["errorDetails"] = fmt.Sprint(v.RawData())
@@ -259,7 +281,7 @@ func ActivityLogger(app core.App) echo.MiddlewareFunc {
// ---
now := time.Now()
lastLogsDeletedAt := cast.ToTime(app.Cache().Get("lastLogsDeletedAt"))
daysDiff := (now.Sub(lastLogsDeletedAt).Hours() * 24)
daysDiff := now.Sub(lastLogsDeletedAt).Hours() * 24
if daysDiff > float64(app.Settings().Logs.MaxDays) {
deleteErr := app.LogsDao().DeleteOldRequests(now.AddDate(0, 0, -1*app.Settings().Logs.MaxDays))
+119
View File
@@ -291,6 +291,125 @@ func TestRequireAdminAuth(t *testing.T) {
}
}
func TestRequireAdminAuthOnlyIfAny(t *testing.T) {
scenarios := []tests.ApiScenario{
{
Name: "guest (while having at least 1 existing admin)",
Method: http.MethodGet,
Url: "/my/test",
BeforeFunc: 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",
BeforeFunc: 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)
}
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: 200,
ExpectedContent: []string{"test123"},
},
{
Name: "expired/invalid token",
Method: http.MethodGet,
Url: "/my/test",
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA",
},
BeforeFunc: 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: "valid user token",
Method: http.MethodGet,
Url: "/my/test",
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
},
BeforeFunc: 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: "valid admin token",
Method: http.MethodGet,
Url: "/my/test",
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
BeforeFunc: 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: 200,
ExpectedContent: []string{"test123"},
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
}
func TestRequireAdminOrUserAuth(t *testing.T) {
scenarios := []tests.ApiScenario{
{
+23 -19
View File
@@ -159,22 +159,23 @@ func (api *realtimeApi) bindEvents() {
adminTable := (&models.Admin{}).TableName()
// update user/admin auth state
api.app.OnModelAfterUpdate().Add(func(data *core.ModelEvent) error {
modelTable := data.Model.TableName()
api.app.OnModelAfterUpdate().Add(func(e *core.ModelEvent) error {
modelTable := e.Model.TableName()
var contextKey string
if modelTable == userTable {
switch modelTable {
case userTable:
contextKey = ContextUserKey
} else if modelTable == adminTable {
case adminTable:
contextKey = ContextAdminKey
} else {
default:
return nil
}
for _, client := range api.app.SubscriptionsBroker().Clients() {
model, _ := client.Get(contextKey).(models.Model)
if model != nil && model.GetId() == data.Model.GetId() {
client.Set(contextKey, data.Model)
if model != nil && model.GetId() == e.Model.GetId() {
client.Set(contextKey, e.Model)
}
}
@@ -182,21 +183,22 @@ func (api *realtimeApi) bindEvents() {
})
// remove user/admin client(s)
api.app.OnModelAfterDelete().Add(func(data *core.ModelEvent) error {
modelTable := data.Model.TableName()
api.app.OnModelAfterDelete().Add(func(e *core.ModelEvent) error {
modelTable := e.Model.TableName()
var contextKey string
if modelTable == userTable {
switch modelTable {
case userTable:
contextKey = ContextUserKey
} else if modelTable == adminTable {
case adminTable:
contextKey = ContextAdminKey
} else {
default:
return nil
}
for _, client := range api.app.SubscriptionsBroker().Clients() {
model, _ := client.Get(contextKey).(models.Model)
if model != nil && model.GetId() == data.Model.GetId() {
if model != nil && model.GetId() == e.Model.GetId() {
api.app.SubscriptionsBroker().Unregister(client.Id())
}
}
@@ -204,18 +206,20 @@ func (api *realtimeApi) bindEvents() {
return nil
})
api.app.OnRecordAfterCreateRequest().Add(func(data *core.RecordCreateEvent) error {
api.broadcastRecord("create", data.Record)
api.app.OnRecordAfterCreateRequest().Add(func(e *core.RecordCreateEvent) error {
api.broadcastRecord("create", e.Record)
return nil
})
api.app.OnRecordAfterUpdateRequest().Add(func(data *core.RecordUpdateEvent) error {
api.broadcastRecord("update", data.Record)
api.app.OnRecordAfterUpdateRequest().Add(func(e *core.RecordUpdateEvent) error {
api.broadcastRecord("update", e.Record)
return nil
})
api.app.OnRecordAfterDeleteRequest().Add(func(data *core.RecordDeleteEvent) error {
api.broadcastRecord("delete", data.Record)
api.app.OnModelBeforeDelete().Add(func(e *core.ModelEvent) error {
if record, ok := e.Model.(*models.Record); ok {
api.broadcastRecord("delete", record)
}
return nil
})
}
+28 -42
View File
@@ -6,8 +6,8 @@ import (
"net/http"
"strings"
"github.com/pocketbase/dbx"
"github.com/labstack/echo/v5"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/forms"
@@ -193,7 +193,7 @@ func (api *recordApi) create(c echo.Context) error {
testRecord := models.NewRecord(collection)
testForm := forms.NewRecordUpsert(api.app, testRecord)
if err := testForm.LoadData(c.Request()); err != nil {
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
}
testErr := testForm.DrySubmit(func(txDao *daos.Dao) error {
@@ -210,7 +210,7 @@ func (api *recordApi) create(c echo.Context) error {
// load request
if err := form.LoadData(c.Request()); err != nil {
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
}
event := &core.RecordCreateEvent{
@@ -218,20 +218,24 @@ func (api *recordApi) create(c echo.Context) error {
Record: record,
}
handlerErr := api.app.OnRecordBeforeCreateRequest().Trigger(event, func(e *core.RecordCreateEvent) error {
// create the record
if err := form.Submit(); err != nil {
return rest.NewBadRequestError("Failed to create record.", err)
}
// create the record
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
return api.app.OnRecordBeforeCreateRequest().Trigger(event, func(e *core.RecordCreateEvent) error {
if err := next(); err != nil {
return rest.NewBadRequestError("Failed to create record.", err)
}
return e.HttpContext.JSON(http.StatusOK, e.Record)
return e.HttpContext.JSON(http.StatusOK, e.Record)
})
}
})
if handlerErr == nil {
if submitErr == nil {
api.app.OnRecordAfterCreateRequest().Trigger(event)
}
return handlerErr
return submitErr
}
func (api *recordApi) update(c echo.Context) error {
@@ -276,7 +280,7 @@ func (api *recordApi) update(c echo.Context) error {
// load request
if err := form.LoadData(c.Request()); err != nil {
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
}
event := &core.RecordUpdateEvent{
@@ -284,20 +288,24 @@ func (api *recordApi) update(c echo.Context) error {
Record: record,
}
handlerErr := api.app.OnRecordBeforeUpdateRequest().Trigger(event, func(e *core.RecordUpdateEvent) error {
// update the record
if err := form.Submit(); err != nil {
return rest.NewBadRequestError("Failed to update record.", err)
}
// update the record
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
return api.app.OnRecordBeforeUpdateRequest().Trigger(event, func(e *core.RecordUpdateEvent) error {
if err := next(); err != nil {
return rest.NewBadRequestError("Failed to update record.", err)
}
return e.HttpContext.JSON(http.StatusOK, e.Record)
return e.HttpContext.JSON(http.StatusOK, e.Record)
})
}
})
if handlerErr == nil {
if submitErr == nil {
api.app.OnRecordAfterUpdateRequest().Trigger(event)
}
return handlerErr
return submitErr
}
func (api *recordApi) delete(c echo.Context) error {
@@ -348,13 +356,6 @@ func (api *recordApi) delete(c echo.Context) error {
return rest.NewBadRequestError("Failed to delete record. Make sure that the record is not part of a required relation reference.", err)
}
// try to delete the record files
if err := api.deleteRecordFiles(e.Record); err != nil && api.app.IsDebug() {
// non critical error - only log for debug
// (usually could happen due to S3 api limits)
log.Println(err)
}
return e.HttpContext.NoContent(http.StatusNoContent)
})
@@ -365,21 +366,6 @@ func (api *recordApi) delete(c echo.Context) error {
return handlerErr
}
func (api *recordApi) deleteRecordFiles(record *models.Record) error {
fs, err := api.app.NewFilesystem()
if err != nil {
return err
}
defer fs.Close()
failed := fs.DeletePrefix(record.BaseFilesPath())
if len(failed) > 0 {
return fmt.Errorf("Failed to delete %d record files.", len(failed))
}
return nil
}
func (api *recordApi) exportRequestData(c echo.Context) map[string]any {
result := map[string]any{}
queryParams := map[string]any{}
+27 -16
View File
@@ -144,12 +144,11 @@ func TestRecordsList(t *testing.T) {
`"manyrels":[{`,
`"manyrels":[]`,
`"rel_cascade":"`,
`"rel_cascade":null`,
`"onerel":{"@collectionId":"3f2888f8-075d-49fe-9d09-ea7e951000dc","@collectionName":"demo",`,
`"json":[1,2,3]`,
`"select":["a","b"]`,
`"select":[]`,
`"user":null`,
`"user":""`,
`"bool":true`,
`"number":456`,
`"user":"97cc3d3d-6ba2-383f-b42a-7bc84d27410c"`,
@@ -455,11 +454,13 @@ func TestRecordDelete(t *testing.T) {
"OnRecordAfterDeleteRequest": 1,
"OnModelAfterUpdate": 1, // nullify related record
"OnModelBeforeUpdate": 1, // nullify related record
"OnModelBeforeDelete": 2, // +1 cascade delete related record
"OnModelAfterDelete": 2, // +1 cascade delete related record
"OnModelBeforeDelete": 3, // +1 cascade delete related record
"OnModelAfterDelete": 3, // +1 cascade delete related record
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
ensureDeletedFiles(app, "3f2888f8-075d-49fe-9d09-ea7e951000dc", "577bd676-aacb-4072-b7da-99d00ee210a4")
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "94568ca2-0bee-49d7-b749-06cb97956fd9")
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "63c2ab80-84ab-4057-a592-4604a731f78f")
},
},
{
@@ -475,11 +476,13 @@ func TestRecordDelete(t *testing.T) {
"OnRecordAfterDeleteRequest": 1,
"OnModelAfterUpdate": 1, // nullify related record
"OnModelBeforeUpdate": 1, // nullify related record
"OnModelBeforeDelete": 2, // +1 cascade delete related record
"OnModelAfterDelete": 2, // +1 cascade delete related record
"OnModelBeforeDelete": 3, // +1 cascade delete related record
"OnModelAfterDelete": 3, // +1 cascade delete related record
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
ensureDeletedFiles(app, "3f2888f8-075d-49fe-9d09-ea7e951000dc", "577bd676-aacb-4072-b7da-99d00ee210a4")
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "94568ca2-0bee-49d7-b749-06cb97956fd9")
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "63c2ab80-84ab-4057-a592-4604a731f78f")
},
},
{
@@ -554,8 +557,8 @@ func TestRecordDelete(t *testing.T) {
"OnRecordAfterDeleteRequest": 1,
"OnModelBeforeUpdate": 1,
"OnModelAfterUpdate": 1,
"OnModelBeforeDelete": 2,
"OnModelAfterDelete": 2,
"OnModelBeforeDelete": 3,
"OnModelAfterDelete": 3,
},
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
recId := "63c2ab80-84ab-4057-a592-4604a731f78f"
@@ -565,6 +568,7 @@ func TestRecordDelete(t *testing.T) {
t.Errorf("Expected record %s to be cascade deleted", recId)
}
ensureDeletedFiles(app, "3f2888f8-075d-49fe-9d09-ea7e951000dc", "577bd676-aacb-4072-b7da-99d00ee210a4")
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "94568ca2-0bee-49d7-b749-06cb97956fd9")
ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "63c2ab80-84ab-4057-a592-4604a731f78f")
},
},
@@ -650,7 +654,8 @@ func TestRecordCreate(t *testing.T) {
"onerel": "577bd676-aacb-4072-b7da-99d00ee210a4",
"manyrels": ["577bd676-aacb-4072-b7da-99d00ee210a4"],
"text": "test123",
"bool": "false"
"bool": "false",
"number": 1
}`),
RequestHeaders: map[string]string{
// test@example.com
@@ -668,7 +673,8 @@ func TestRecordCreate(t *testing.T) {
"onerel":"577bd676-aacb-4072-b7da-99d00ee210a4",
"manyrels":["577bd676-aacb-4072-b7da-99d00ee210a4"],
"text":"test123",
"bool":true
"bool":true,
"number":1
}`),
RequestHeaders: map[string]string{
// test3@example.com
@@ -682,6 +688,7 @@ func TestRecordCreate(t *testing.T) {
`"manyrels":["577bd676-aacb-4072-b7da-99d00ee210a4"]`,
`"text":"test123"`,
`"bool":true`,
`"number":1`,
},
ExpectedEvents: map[string]int{
"OnRecordBeforeCreateRequest": 1,
@@ -695,11 +702,12 @@ func TestRecordCreate(t *testing.T) {
Method: http.MethodPost,
Url: "/api/collections/demo2/records",
Body: strings.NewReader(`{
"rel_cascade":"577bd676-aacb-4072-b7da-99d00ee210a4",
"onerel":"577bd676-aacb-4072-b7da-99d00ee210a4",
"manyrels":["577bd676-aacb-4072-b7da-99d00ee210a4"],
"text":"test123",
"bool":false
"rel_cascade": "577bd676-aacb-4072-b7da-99d00ee210a4",
"onerel": "577bd676-aacb-4072-b7da-99d00ee210a4",
"manyrels" :["577bd676-aacb-4072-b7da-99d00ee210a4"],
"text": "test123",
"bool": false,
"number": 1
}`),
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
@@ -712,6 +720,7 @@ func TestRecordCreate(t *testing.T) {
`"manyrels":["577bd676-aacb-4072-b7da-99d00ee210a4"]`,
`"text":"test123"`,
`"bool":false`,
`"number":1`,
},
ExpectedEvents: map[string]int{
"OnRecordBeforeCreateRequest": 1,
@@ -867,7 +876,8 @@ func TestRecordUpdate(t *testing.T) {
Method: http.MethodPatch,
Url: "/api/collections/demo2/records/63c2ab80-84ab-4057-a592-4604a731f78f",
Body: strings.NewReader(`{
"text":"test_new"
"text":"test_new",
"number":1
}`),
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
@@ -876,6 +886,7 @@ func TestRecordUpdate(t *testing.T) {
ExpectedContent: []string{
`"id":"63c2ab80-84ab-4057-a592-4604a731f78f"`,
`"text":"test_new"`,
`"number":1`,
},
ExpectedEvents: map[string]int{
"OnRecordBeforeUpdateRequest": 1,
+19 -12
View File
@@ -40,8 +40,10 @@ func (api *settingsApi) list(c echo.Context) error {
func (api *settingsApi) set(c echo.Context) error {
form := forms.NewSettingsUpsert(api.app)
// load request
if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("An error occured while reading the submitted data.", err)
return rest.NewBadRequestError("An error occurred while reading the submitted data.", err)
}
event := &core.SettingsUpdateEvent{
@@ -50,22 +52,27 @@ func (api *settingsApi) set(c echo.Context) error {
NewSettings: form.Settings,
}
handlerErr := api.app.OnSettingsBeforeUpdateRequest().Trigger(event, func(e *core.SettingsUpdateEvent) error {
if err := form.Submit(); err != nil {
return rest.NewBadRequestError("An error occured while submitting the form.", err)
}
// update the settings
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
return api.app.OnSettingsBeforeUpdateRequest().Trigger(event, func(e *core.SettingsUpdateEvent) error {
if err := next(); err != nil {
return rest.NewBadRequestError("An error occurred while submitting the form.", err)
}
redactedSettings, err := api.app.Settings().RedactClone()
if err != nil {
return rest.NewBadRequestError("", err)
}
redactedSettings, err := api.app.Settings().RedactClone()
if err != nil {
return rest.NewBadRequestError("", err)
}
return e.HttpContext.JSON(http.StatusOK, redactedSettings)
return e.HttpContext.JSON(http.StatusOK, redactedSettings)
})
}
})
if handlerErr == nil {
if submitErr == nil {
api.app.OnSettingsAfterUpdateRequest().Trigger(event)
}
return handlerErr
return submitErr
}
-3
View File
@@ -139,9 +139,6 @@ func TestSettingsSet(t *testing.T) {
`"emailAuth":{"minPasswordLength":{"code":"validation_min_greater_equal_than_required","message":"Must be no less than 5."}}`,
`"meta":{"appName":{"code":"validation_required","message":"Cannot be blank."}}`,
},
ExpectedEvents: map[string]int{
"OnSettingsBeforeUpdateRequest": 1,
},
},
{
Name: "authorized as admin submitting valid data",
+37 -29
View File
@@ -152,7 +152,7 @@ func (api *userApi) authMethods(c echo.Context) error {
func (api *userApi) oauth2Auth(c echo.Context) error {
form := forms.NewUserOauth2Login(api.app)
if readErr := c.Bind(form); readErr != nil {
return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr)
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
}
user, authData, submitErr := form.Submit()
@@ -170,7 +170,7 @@ func (api *userApi) emailAuth(c echo.Context) error {
form := forms.NewUserEmailLogin(api.app)
if readErr := c.Bind(form); readErr != nil {
return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr)
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
}
user, submitErr := form.Submit()
@@ -184,11 +184,11 @@ func (api *userApi) emailAuth(c echo.Context) error {
func (api *userApi) requestPasswordReset(c echo.Context) error {
form := forms.NewUserPasswordResetRequest(api.app)
if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("An error occured while reading the submitted data.", err)
return rest.NewBadRequestError("An error occurred while reading the submitted data.", err)
}
if err := form.Validate(); err != nil {
return rest.NewBadRequestError("An error occured while validating the form.", err)
return rest.NewBadRequestError("An error occurred while validating the form.", err)
}
// run in background because we don't need to show
@@ -205,7 +205,7 @@ func (api *userApi) requestPasswordReset(c echo.Context) error {
func (api *userApi) confirmPasswordReset(c echo.Context) error {
form := forms.NewUserPasswordResetConfirm(api.app)
if readErr := c.Bind(form); readErr != nil {
return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr)
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
}
user, submitErr := form.Submit()
@@ -224,7 +224,7 @@ func (api *userApi) requestEmailChange(c echo.Context) error {
form := forms.NewUserEmailChangeRequest(api.app, loggedUser)
if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("An error occured while reading the submitted data.", err)
return rest.NewBadRequestError("An error occurred while reading the submitted data.", err)
}
if err := form.Submit(); err != nil {
@@ -237,7 +237,7 @@ func (api *userApi) requestEmailChange(c echo.Context) error {
func (api *userApi) confirmEmailChange(c echo.Context) error {
form := forms.NewUserEmailChangeConfirm(api.app)
if readErr := c.Bind(form); readErr != nil {
return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr)
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
}
user, submitErr := form.Submit()
@@ -251,11 +251,11 @@ func (api *userApi) confirmEmailChange(c echo.Context) error {
func (api *userApi) requestVerification(c echo.Context) error {
form := forms.NewUserVerificationRequest(api.app)
if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("An error occured while reading the submitted data.", err)
return rest.NewBadRequestError("An error occurred while reading the submitted data.", err)
}
if err := form.Validate(); err != nil {
return rest.NewBadRequestError("An error occured while validating the form.", err)
return rest.NewBadRequestError("An error occurred while validating the form.", err)
}
// run in background because we don't need to show
@@ -272,12 +272,12 @@ func (api *userApi) requestVerification(c echo.Context) error {
func (api *userApi) confirmVerification(c echo.Context) error {
form := forms.NewUserVerificationConfirm(api.app)
if readErr := c.Bind(form); readErr != nil {
return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr)
return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr)
}
user, submitErr := form.Submit()
if submitErr != nil {
return rest.NewBadRequestError("An error occured while submitting the form.", submitErr)
return rest.NewBadRequestError("An error occurred while submitting the form.", submitErr)
}
return api.authResponse(c, user, nil)
@@ -348,7 +348,7 @@ func (api *userApi) create(c echo.Context) error {
// load request
if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
}
event := &core.UserCreateEvent{
@@ -356,20 +356,24 @@ func (api *userApi) create(c echo.Context) error {
User: user,
}
handlerErr := api.app.OnUserBeforeCreateRequest().Trigger(event, func(e *core.UserCreateEvent) error {
// create the user
if err := form.Submit(); err != nil {
return rest.NewBadRequestError("Failed to create user.", err)
}
// create the user
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
return api.app.OnUserBeforeCreateRequest().Trigger(event, func(e *core.UserCreateEvent) error {
if err := next(); err != nil {
return rest.NewBadRequestError("Failed to create user.", err)
}
return e.HttpContext.JSON(http.StatusOK, e.User)
return e.HttpContext.JSON(http.StatusOK, e.User)
})
}
})
if handlerErr == nil {
if submitErr == nil {
api.app.OnUserAfterCreateRequest().Trigger(event)
}
return handlerErr
return submitErr
}
func (api *userApi) update(c echo.Context) error {
@@ -387,7 +391,7 @@ func (api *userApi) update(c echo.Context) error {
// load request
if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err)
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
}
event := &core.UserUpdateEvent{
@@ -395,20 +399,24 @@ func (api *userApi) update(c echo.Context) error {
User: user,
}
handlerErr := api.app.OnUserBeforeUpdateRequest().Trigger(event, func(e *core.UserUpdateEvent) error {
// update the user
if err := form.Submit(); err != nil {
return rest.NewBadRequestError("Failed to update user.", err)
}
// update the user
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
return api.app.OnUserBeforeUpdateRequest().Trigger(event, func(e *core.UserUpdateEvent) error {
if err := next(); err != nil {
return rest.NewBadRequestError("Failed to update user.", err)
}
return e.HttpContext.JSON(http.StatusOK, e.User)
return e.HttpContext.JSON(http.StatusOK, e.User)
})
}
})
if handlerErr == nil {
if submitErr == nil {
api.app.OnUserAfterUpdateRequest().Trigger(event)
}
return handlerErr
return submitErr
}
func (api *userApi) delete(c echo.Context) error {
-9
View File
@@ -748,9 +748,6 @@ func TestUserCreate(t *testing.T) {
`"email":{"code":"validation_required"`,
`"password":{"code":"validation_required"`,
},
ExpectedEvents: map[string]int{
"OnUserBeforeCreateRequest": 1,
},
},
{
Name: "invalid data",
@@ -764,9 +761,6 @@ func TestUserCreate(t *testing.T) {
`"password":{"code":"validation_length_out_of_range"`,
`"passwordConfirm":{"code":"validation_values_mismatch"`,
},
ExpectedEvents: map[string]int{
"OnUserBeforeCreateRequest": 1,
},
},
{
Name: "valid data but with disabled email/pass auth",
@@ -868,9 +862,6 @@ func TestUserUpdate(t *testing.T) {
`"data":{`,
`"email":{"code":"validation_user_email_exists"`,
},
ExpectedEvents: map[string]int{
"OnUserBeforeUpdateRequest": 1,
},
},
{
Name: "authorized as admin - valid data",
+10 -83
View File
@@ -2,7 +2,6 @@ package cmd
import (
"crypto/tls"
"errors"
"fmt"
"log"
"net"
@@ -10,14 +9,10 @@ import (
"path/filepath"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/fatih/color"
"github.com/go-ozzo/ozzo-validation/v4/is"
"github.com/labstack/echo/v5/middleware"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/forms"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tools/migrate"
"github.com/spf13/cobra"
"golang.org/x/crypto/acme"
@@ -35,18 +30,6 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command {
Use: "serve",
Short: "Starts the web server (default to localhost:8090)",
Run: func(command *cobra.Command, args []string) {
router, err := apis.InitApi(app)
if err != nil {
panic(err)
}
// configure cors
router.Use(middleware.CORSWithConfig(middleware.CORSConfig(middleware.CORSConfig{
Skipper: middleware.DefaultSkipper,
AllowOrigins: allowedOrigins,
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
})))
// ensure that the latest migrations are applied before starting the server
if err := runMigrations(app); err != nil {
panic(err)
@@ -61,19 +44,18 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command {
color.Yellow("=====================================")
}
// if no admins are found, create the first one
totalAdmins, err := app.Dao().TotalAdmins()
router, err := apis.InitApi(app)
if err != nil {
log.Fatalln(err)
return
}
if totalAdmins == 0 {
if err := promptCreateAdmin(app); err != nil {
log.Fatalln(err)
return
}
panic(err)
}
// configure cors
router.Use(middleware.CORSWithConfig(middleware.CORSConfig{
Skipper: middleware.DefaultSkipper,
AllowOrigins: allowedOrigins,
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
}))
// start http server
// ---
mainAddr := httpAddr
@@ -149,7 +131,7 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command {
&httpsAddr,
"https",
"",
"api HTTPS server address (auto TLS via Let's Encrypt)\nthe incomming --http address traffic also will be redirected to this address",
"api HTTPS server address (auto TLS via Let's Encrypt)\nthe incoming --http address traffic also will be redirected to this address",
)
return command
@@ -171,58 +153,3 @@ func runMigrations(app core.App) error {
return nil
}
func promptCreateAdmin(app core.App) error {
color.White("-------------------------------------")
color.Cyan("Lets create your first admin account:")
color.White("-------------------------------------")
prompts := []*survey.Question{
{
Name: "Email",
Prompt: &survey.Input{Message: "Email:"},
Validate: func(val any) error {
if err := survey.Required(val); err != nil {
return err
}
if err := is.Email.Validate(val); err != nil {
return err
}
return nil
},
},
{
Name: "Password",
Prompt: &survey.Password{Message: "Pass (min 10 chars):"},
Validate: func(val any) error {
if str, ok := val.(string); !ok || len(str) < 10 {
return errors.New("The password must be at least 10 characters.")
}
return nil
},
},
}
result := struct {
Email string
Password string
}{}
if err := survey.Ask(prompts, &result); err != nil {
return err
}
form := forms.NewAdminUpsert(app, &models.Admin{})
form.Email = result.Email
form.Password = result.Password
form.PasswordConfirm = result.Password
if err := form.Submit(); err != nil {
return err
}
color.Green("Successfully created admin %s!", result.Email)
fmt.Println("")
return nil
}
-21
View File
@@ -1,21 +0,0 @@
// Package cmd implements various PocketBase system commands.
package cmd
import (
"fmt"
"github.com/pocketbase/pocketbase/core"
"github.com/spf13/cobra"
)
// NewVersionCommand creates and returns new command that prints
// the current PocketBase version.
func NewVersionCommand(app core.App, version string) *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Prints the current PocketBase app version",
Run: func(command *cobra.Command, args []string) {
fmt.Printf("PocketBase v%s\n", version)
},
}
}
+8 -8
View File
@@ -91,7 +91,7 @@ type App interface {
// entry in the DB, allowing you to modify or validate the stored data.
OnModelBeforeCreate() *hook.Hook[*ModelEvent]
// OnModelAfterCreate hook is triggered after successfuly
// OnModelAfterCreate hook is triggered after successfully
// inserting a new entry in the DB.
OnModelAfterCreate() *hook.Hook[*ModelEvent]
@@ -99,7 +99,7 @@ type App interface {
// entry in the DB, allowing you to modify or validate the stored data.
OnModelBeforeUpdate() *hook.Hook[*ModelEvent]
// OnModelAfterUpdate hook is triggered after successfuly updating
// OnModelAfterUpdate hook is triggered after successfully updating
// existing entry in the DB.
OnModelAfterUpdate() *hook.Hook[*ModelEvent]
@@ -107,7 +107,7 @@ type App interface {
// existing entry from the DB.
OnModelBeforeDelete() *hook.Hook[*ModelEvent]
// OnModelAfterDelete is triggered after successfuly deleting an
// OnModelAfterDelete is triggered after successfully deleting an
// existing entry from the DB.
OnModelAfterDelete() *hook.Hook[*ModelEvent]
@@ -123,7 +123,7 @@ type App interface {
OnMailerBeforeAdminResetPasswordSend() *hook.Hook[*MailerAdminEvent]
// OnMailerAfterAdminResetPasswordSend hook is triggered after
// admin password reset email was successfuly sent.
// admin password reset email was successfully sent.
OnMailerAfterAdminResetPasswordSend() *hook.Hook[*MailerAdminEvent]
// OnMailerBeforeUserResetPasswordSend hook is triggered right before
@@ -134,7 +134,7 @@ type App interface {
OnMailerBeforeUserResetPasswordSend() *hook.Hook[*MailerUserEvent]
// OnMailerAfterUserResetPasswordSend hook is triggered after
// a user password reset email was successfuly sent.
// a user password reset email was successfully sent.
OnMailerAfterUserResetPasswordSend() *hook.Hook[*MailerUserEvent]
// OnMailerBeforeUserVerificationSend hook is triggered right before
@@ -145,7 +145,7 @@ type App interface {
OnMailerBeforeUserVerificationSend() *hook.Hook[*MailerUserEvent]
// OnMailerAfterUserVerificationSend hook is triggered after a user
// verification email was successfuly sent.
// verification email was successfully sent.
OnMailerAfterUserVerificationSend() *hook.Hook[*MailerUserEvent]
// OnMailerBeforeUserChangeEmailSend hook is triggered right before
@@ -156,7 +156,7 @@ type App interface {
OnMailerBeforeUserChangeEmailSend() *hook.Hook[*MailerUserEvent]
// OnMailerAfterUserChangeEmailSend hook is triggered after a user
// change address email was successfuly sent.
// change address email was successfully sent.
OnMailerAfterUserChangeEmailSend() *hook.Hook[*MailerUserEvent]
// ---------------------------------------------------------------
@@ -180,7 +180,7 @@ type App interface {
// Settings API event hooks
// ---------------------------------------------------------------
// OnSettingsListRequest hook is triggered on each successfull
// OnSettingsListRequest hook is triggered on each successful
// API Settings list request.
//
// Could be used to validate or modify the response before
+36 -1
View File
@@ -5,6 +5,7 @@ import (
"database/sql"
"encoding/json"
"errors"
"log"
"os"
"path/filepath"
"time"
@@ -123,7 +124,7 @@ type BaseApp struct {
//
// To initialize the app, you need to call `app.Bootsrap()`.
func NewBaseApp(dataDir string, encryptionEnv string, isDebug bool) *BaseApp {
return &BaseApp{
app := &BaseApp{
dataDir: dataDir,
isDebug: isDebug,
encryptionEnv: encryptionEnv,
@@ -209,6 +210,10 @@ func NewBaseApp(dataDir string, encryptionEnv string, isDebug bool) *BaseApp {
onCollectionBeforeDeleteRequest: &hook.Hook[*CollectionDeleteEvent]{},
onCollectionAfterDeleteRequest: &hook.Hook[*CollectionDeleteEvent]{},
}
app.registerDefaultHooks()
return app
}
// Bootstrap initializes the application
@@ -750,3 +755,33 @@ func (app *BaseApp) createDao(db dbx.Builder) *daos.Dao {
return dao
}
func (app *BaseApp) registerDefaultHooks() {
deletePrefix := func(prefix string) error {
fs, err := app.NewFilesystem()
if err != nil {
return err
}
defer fs.Close()
failed := fs.DeletePrefix(prefix)
if len(failed) > 0 {
return errors.New("Failed to delete the files at " + prefix)
}
return nil
}
// delete storage files from deleted Collection, Records, etc.
app.OnModelAfterDelete().Add(func(e *ModelEvent) error {
if m, ok := e.Model.(models.FilesManager); ok && m.BaseFilesPath() != "" {
if err := deletePrefix(m.BaseFilesPath()); err != nil && app.IsDebug() {
// non critical error - only log for debug
// (usually could happen because of S3 api limits)
log.Println(err)
}
}
return nil
})
}
+1 -1
View File
@@ -5,8 +5,8 @@ package core
import (
"fmt"
"github.com/pocketbase/dbx"
_ "github.com/mattn/go-sqlite3"
"github.com/pocketbase/dbx"
)
func connectDB(dbPath string) (*dbx.DB, error) {
+7 -7
View File
@@ -148,12 +148,12 @@ func (s *Settings) Merge(other *Settings) error {
}
// Clone creates a new deep copy of the current settings.
func (c *Settings) Clone() (*Settings, error) {
new := &Settings{}
if err := new.Merge(c); err != nil {
func (s *Settings) Clone() (*Settings, error) {
settings := &Settings{}
if err := settings.Merge(s); err != nil {
return nil, err
}
return new, nil
return settings, nil
}
// RedactClone creates a new deep copy of the current settings,
@@ -255,7 +255,7 @@ type S3Config struct {
// Validate makes S3Config validatable by implementing [validation.Validatable] interface.
func (c S3Config) Validate() error {
return validation.ValidateStruct(&c,
validation.Field(&c.Endpoint, is.Host, validation.When(c.Enabled, validation.Required)),
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)),
@@ -360,11 +360,11 @@ func (c AuthProviderConfig) SetupProvider(provider auth.Provider) error {
}
if c.ClientId != "" {
provider.SetClientId(string(c.ClientId))
provider.SetClientId(c.ClientId)
}
if c.ClientSecret != "" {
provider.SetClientSecret(string(c.ClientSecret))
provider.SetClientSecret(c.ClientSecret)
}
if c.AuthUrl != "" {
+13 -1
View File
@@ -315,7 +315,19 @@ func TestS3ConfigValidate(t *testing.T) {
},
true,
},
// valid data
// valid data (url endpoint)
{
core.S3Config{
Enabled: true,
Endpoint: "https://localhost:8090",
Bucket: "test",
Region: "test",
AccessKey: "test",
Secret: "test",
},
false,
},
// valid data (hostname endpoint)
{
core.S3Config{
Enabled: true,
+1 -1
View File
@@ -45,7 +45,7 @@ func (dao *Dao) FindAdminByEmail(email string) (*models.Admin, error) {
return model, nil
}
// FindAdminByEmail finds the admin associated with the provided JWT token.
// FindAdminByToken finds the admin associated with the provided JWT token.
//
// Returns an error if the JWT token is invalid or expired.
func (dao *Dao) FindAdminByToken(token string, baseTokenKey string) (*models.Admin, error) {
+4 -7
View File
@@ -115,9 +115,8 @@ func (dao *Dao) Delete(m models.Model) error {
}
}
deleteErr := dao.db.Model(m).Delete()
if deleteErr != nil {
return deleteErr
if err := dao.db.Model(m).Delete(); err != nil {
return err
}
if dao.AfterDeleteFunc != nil {
@@ -162,8 +161,7 @@ func (dao *Dao) update(m models.Model) error {
return err
}
} else {
err := dao.db.Model(m).Update()
if err != nil {
if err := dao.db.Model(m).Update(); err != nil {
return err
}
}
@@ -203,8 +201,7 @@ func (dao *Dao) create(m models.Model) error {
return err
}
} else {
err := dao.db.Model(m).Insert()
if err != nil {
if err := dao.db.Model(m).Insert(); err != nil {
return err
}
}
-1
View File
@@ -166,7 +166,6 @@ func TestDeleteCollection(t *testing.T) {
t.Errorf("(%d) Expected hasErr %v, got %v", i, scenario.expectError, hasErr)
}
}
}
func TestSaveCollectionCreate(t *testing.T) {
+1 -3
View File
@@ -40,7 +40,7 @@ func (dao *Dao) SaveParam(key string, value any, optEncryptionKey ...string) err
param = &models.Param{Key: key}
}
var normalizedValue any
normalizedValue := value
// encrypt if optEncryptionKey is set
if len(optEncryptionKey) > 0 && optEncryptionKey[0] != "" {
@@ -55,8 +55,6 @@ func (dao *Dao) SaveParam(key string, value any, optEncryptionKey ...string) err
}
normalizedValue = encryptVal
} else {
normalizedValue = value
}
encodedValue := types.JsonRaw{}
+29 -7
View File
@@ -9,6 +9,7 @@ import (
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/tools/list"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/pocketbase/pocketbase/tools/types"
)
@@ -309,7 +310,7 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle
newSchema := newCollection.Schema
// check for renamed table
if strings.ToLower(oldTableName) != strings.ToLower(newTableName) {
if !strings.EqualFold(oldTableName, newTableName) {
_, err := dao.DB().RenameTable(oldTableName, newTableName).Execute()
if err != nil {
return err
@@ -329,23 +330,44 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle
}
// check for new or renamed columns
toRename := map[string]string{}
for _, field := range newSchema.Fields() {
oldField := oldSchema.GetFieldById(field.Id)
if oldField != nil {
// rename
_, err := txDao.DB().RenameColumn(newTableName, oldField.Name, field.Name).Execute()
// 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.RandomString(5)
toRename[tempName] = field.Name
// add
_, err := txDao.DB().AddColumn(newTableName, tempName, field.ColDefinition()).Execute()
if err != nil {
return err
}
} else {
// add
_, err := txDao.DB().AddColumn(newTableName, field.Name, field.ColDefinition()).Execute()
} else if oldField.Name != field.Name {
tempName := field.Name + security.RandomString(5)
toRename[tempName] = field.Name
// rename
_, err := txDao.DB().RenameColumn(newTableName, oldField.Name, tempName).Execute()
if err != nil {
return err
}
}
}
// set the actual columns name
for tempName, actualName := range toRename {
_, err := txDao.DB().RenameColumn(newTableName, tempName, actualName).Execute()
if err != nil {
return err
}
}
return nil
})
}
+8 -10
View File
@@ -64,7 +64,7 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch
}
// extract the id of the relations to expand
relIds := []string{}
relIds := make([]string, 0, len(records))
for _, record := range records {
relIds = append(relIds, record.GetStringSliceDataValue(relField.Name)...)
}
@@ -92,7 +92,7 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch
for _, model := range records {
relIds := model.GetStringSliceDataValue(relField.Name)
validRels := []*models.Record{}
validRels := make([]*models.Record, 0, len(relIds))
for _, id := range relIds {
if rel, ok := indexedRels[id]; ok {
validRels = append(validRels, rel)
@@ -120,20 +120,18 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch
// normalizeExpands normalizes expand strings and merges self containing paths
// (eg. ["a.b.c", "a.b", " test ", " ", "test"] -> ["a.b.c", "test"]).
func normalizeExpands(paths []string) []string {
result := []string{}
// normalize paths
normalized := []string{}
normalized := make([]string, 0, len(paths))
for _, p := range paths {
p := strings.ReplaceAll(p, " ", "") // replace spaces
p = strings.Trim(p, ".") // trim incomplete paths
if p == "" {
continue
p = strings.ReplaceAll(p, " ", "") // replace spaces
p = strings.Trim(p, ".") // trim incomplete paths
if p != "" {
normalized = append(normalized, p)
}
normalized = append(normalized, p)
}
// merge containing paths
result := make([]string, 0, len(normalized))
for i, p1 := range normalized {
var skip bool
for j, p2 := range normalized {
+3
View File
@@ -396,6 +396,9 @@ func TestSyncRecordTableSchema(t *testing.T) {
t.Fatal(err)
}
updatedCollection, err := app.Dao().FindCollectionByNameOrId("demo")
if err != nil {
t.Fatal(err)
}
updatedCollection.Name = "demo_renamed"
updatedCollection.Schema.RemoveField(updatedCollection.Schema.GetFieldByName("file").Id)
updatedCollection.Schema.AddField(
+3 -3
View File
@@ -43,10 +43,10 @@ func (dao *Dao) LoadProfiles(users []*models.User) error {
}
// extract user ids
ids := []string{}
ids := make([]string, len(users))
usersMap := map[string]*models.User{}
for _, user := range users {
ids = append(ids, user.Id)
for i, user := range users {
ids[i] = user.Id
usersMap[user.Id] = user
}
+1 -1
View File
@@ -107,7 +107,7 @@ func TestAdminPasswordResetConfirmSubmit(t *testing.T) {
}
claims, _ := security.ParseUnverifiedJWT(s.token)
tokenAdminId, _ := claims["id"]
tokenAdminId := claims["id"]
if admin.Id != tokenAdminId {
t.Errorf("(%d) Expected admin with id %s to be returned, got %v", i, tokenAdminId, admin)
+8 -3
View File
@@ -74,8 +74,11 @@ func (form *AdminUpsert) checkUniqueEmail(value any) error {
return validation.NewError("validation_admin_email_exists", "Admin email already exists.")
}
// Submit validates the form and upserts the form's admin model.
func (form *AdminUpsert) Submit() error {
// 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) error {
if err := form.Validate(); err != nil {
return err
}
@@ -87,5 +90,7 @@ func (form *AdminUpsert) Submit() error {
form.admin.SetPassword(form.Password)
}
return form.app.Dao().SaveAdmin(form.admin)
return runInterceptors(func() error {
return form.app.Dao().SaveAdmin(form.admin)
}, interceptors...)
}
+65 -1
View File
@@ -2,6 +2,7 @@ package forms_test
import (
"encoding/json"
"errors"
"testing"
validation "github.com/go-ozzo/ozzo-validation/v4"
@@ -252,7 +253,14 @@ func TestAdminUpsertSubmit(t *testing.T) {
continue
}
err := form.Submit()
interceptorCalls := 0
err := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
interceptorCalls++
return next()
}
})
hasErr := err != nil
if hasErr != s.expectError {
@@ -266,6 +274,14 @@ func TestAdminUpsertSubmit(t *testing.T) {
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
}
@@ -283,3 +299,51 @@ func TestAdminUpsertSubmit(t *testing.T) {
}
}
}
func TestAdminUpsertSubmitInterceptors(t *testing.T) {
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) forms.InterceptorNextFunc {
return func() error {
interceptor1Called = true
return next()
}
}
interceptor2Called := false
interceptor2 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() 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")
}
}
+19
View File
@@ -0,0 +1,19 @@
// Package models implements various services used for request data
// validation and applying changes to existing DB models through the app Dao.
package forms
// InterceptorNextFunc is a interceptor handler function.
// Usually used in combination with InterceptorFunc.
type InterceptorNextFunc = func() error
// InterceptorFunc defines a single interceptor function that will execute the provided next func handler.
type InterceptorFunc func(next InterceptorNextFunc) InterceptorNextFunc
// runInterceptors executes the provided list of interceptors.
func runInterceptors(next InterceptorNextFunc, interceptors ...InterceptorFunc) error {
for i := len(interceptors) - 1; i >= 0; i-- {
next = interceptors[i](next)
}
return next()
}
+8 -18
View File
@@ -78,7 +78,6 @@ func (form *CollectionUpsert) Validate() error {
&form.Schema,
validation.By(form.ensureNoSystemFieldsChange),
validation.By(form.ensureNoFieldsTypeChange),
validation.By(form.ensureNoFieldsNameReuse),
),
validation.Field(&form.ListRule, validation.By(form.checkRule)),
validation.Field(&form.ViewRule, validation.By(form.checkRule)),
@@ -95,7 +94,7 @@ func (form *CollectionUpsert) checkUniqueName(value any) error {
return validation.NewError("validation_collection_name_exists", "Collection name must be unique (case insensitive).")
}
if (form.isCreate || strings.ToLower(v) != strings.ToLower(form.collection.Name)) && form.app.Dao().HasTable(v) {
if (form.isCreate || !strings.EqualFold(v, form.collection.Name)) && form.app.Dao().HasTable(v) {
return validation.NewError("validation_collection_name_table_exists", "The collection name must be also unique table name.")
}
@@ -154,20 +153,6 @@ func (form *CollectionUpsert) ensureNoSystemFieldsChange(value any) error {
return nil
}
func (form *CollectionUpsert) ensureNoFieldsNameReuse(value any) error {
v, _ := value.(schema.Schema)
for _, field := range v.Fields() {
oldField := form.collection.Schema.GetFieldByName(field.Name)
if oldField != nil && oldField.Id != field.Id {
return validation.NewError("validation_field_old_field_exist", "Cannot use existing schema field names when renaming fields.")
}
}
return nil
}
func (form *CollectionUpsert) checkRule(value any) error {
v, _ := value.(*string)
@@ -189,7 +174,10 @@ func (form *CollectionUpsert) checkRule(value any) error {
// Submit validates the form and upserts the form's Collection model.
//
// On success the related record table schema will be auto updated.
func (form *CollectionUpsert) Submit() error {
//
// You can optionally provide a list of InterceptorFunc to further
// modify the form behavior before persisting it.
func (form *CollectionUpsert) Submit(interceptors ...InterceptorFunc) error {
if err := form.Validate(); err != nil {
return err
}
@@ -211,5 +199,7 @@ func (form *CollectionUpsert) Submit() error {
form.collection.UpdateRule = form.UpdateRule
form.collection.DeleteRule = form.DeleteRule
return form.app.Dao().SaveCollection(form.collection)
return runInterceptors(func() error {
return form.app.Dao().SaveCollection(form.collection)
}, interceptors...)
}
+71 -3
View File
@@ -2,6 +2,7 @@ package forms_test
import (
"encoding/json"
"errors"
"testing"
validation "github.com/go-ozzo/ozzo-validation/v4"
@@ -260,7 +261,7 @@ func TestCollectionUpsertSubmit(t *testing.T) {
}`,
[]string{"schema"},
},
// update failure - rename fields to existing field names (aka. reusing field names)
// update success - rename fields to existing field names (aka. reusing field names)
{
"test_new",
`{
@@ -269,7 +270,7 @@ func TestCollectionUpsertSubmit(t *testing.T) {
{"id":"b123456","name":"test1","type":"email"}
]
}`,
[]string{"schema"},
[]string{},
},
// update failure - existing name
{
@@ -387,14 +388,31 @@ func TestCollectionUpsertSubmit(t *testing.T) {
continue
}
interceptorCalls := 0
interceptor := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
interceptorCalls++
return next()
}
}
// parse errors
result := form.Submit()
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)
@@ -450,3 +468,53 @@ func TestCollectionUpsertSubmit(t *testing.T) {
}
}
}
func TestCollectionUpsertSubmitInterceptors(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
collection, err := app.Dao().FindCollectionByNameOrId("demo")
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) forms.InterceptorNextFunc {
return func() error {
interceptor1Called = true
return next()
}
}
interceptor2Called := false
interceptor2 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() 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")
}
}
+99 -83
View File
@@ -3,6 +3,7 @@ package forms
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"regexp"
"strconv"
@@ -44,7 +45,6 @@ func NewRecordUpsert(app core.App, record *models.Record) *RecordUpsert {
form.Data = map[string]any{}
for _, field := range record.Collection().Schema.Fields() {
form.Data[field.Name] = record.GetDataValue(field.Name)
}
return form
@@ -147,79 +147,80 @@ func (form *RecordUpsert) LoadData(r *http.Request) error {
for _, field := range form.record.Collection().Schema.Fields() {
key := field.Name
value, _ := extendedData[key]
value := extendedData[key]
value = field.PrepareValue(value)
if field.Type == schema.FieldTypeFile {
options, _ := field.Options.(*schema.FileOptions)
oldNames := list.ToUniqueStringSlice(form.Data[key])
if field.Type != schema.FieldTypeFile {
form.Data[key] = value
continue
}
// delete previously uploaded file(s)
if options.MaxSelect == 1 {
// search for unset zero indexed key as a fallback
indexedKeyValue, hasIndexedKey := extendedData[key+".0"]
options, _ := field.Options.(*schema.FileOptions)
oldNames := list.ToUniqueStringSlice(form.Data[key])
if cast.ToString(value) == "" || (hasIndexedKey && cast.ToString(indexedKeyValue) == "") {
if len(oldNames) > 0 {
form.filesToDelete = append(form.filesToDelete, oldNames...)
}
form.Data[key] = nil
// delete previously uploaded file(s)
if options.MaxSelect == 1 {
// search for unset zero indexed key as a fallback
indexedKeyValue, hasIndexedKey := extendedData[key+".0"]
if cast.ToString(value) == "" || (hasIndexedKey && cast.ToString(indexedKeyValue) == "") {
if len(oldNames) > 0 {
form.filesToDelete = append(form.filesToDelete, oldNames...)
}
} else if options.MaxSelect > 1 {
// search for individual file index to delete (eg. "file.0")
keyExp, _ := regexp.Compile(`^` + regexp.QuoteMeta(key) + `\.\d+$`)
indexesToDelete := []int{}
for indexedKey := range extendedData {
if keyExp.MatchString(indexedKey) && cast.ToString(extendedData[indexedKey]) == "" {
index, indexErr := strconv.Atoi(indexedKey[len(key)+1:])
if indexErr != nil || index >= len(oldNames) {
continue
}
indexesToDelete = append(indexesToDelete, index)
}
}
// slice to fill only with the non-deleted indexes
nonDeleted := []string{}
for i, name := range oldNames {
// not marked for deletion
if !list.ExistInSlice(i, indexesToDelete) {
nonDeleted = append(nonDeleted, name)
form.Data[key] = ""
}
} else if options.MaxSelect > 1 {
// search for individual file index to delete (eg. "file.0")
keyExp, _ := regexp.Compile(`^` + regexp.QuoteMeta(key) + `\.\d+$`)
indexesToDelete := make([]int, 0, len(extendedData))
for indexedKey := range extendedData {
if keyExp.MatchString(indexedKey) && cast.ToString(extendedData[indexedKey]) == "" {
index, indexErr := strconv.Atoi(indexedKey[len(key)+1:])
if indexErr != nil || index >= len(oldNames) {
continue
}
// store the id to actually delete the file later
form.filesToDelete = append(form.filesToDelete, name)
indexesToDelete = append(indexesToDelete, index)
}
form.Data[key] = nonDeleted
}
// check if there are any new uploaded form files
files, err := rest.FindUploadedFiles(r, key)
if err != nil {
continue // skip invalid or missing file(s)
}
// refresh oldNames list
oldNames = list.ToUniqueStringSlice(form.Data[key])
if options.MaxSelect == 1 {
// delete previous file(s) before replacing
if len(oldNames) > 0 {
form.filesToDelete = list.ToUniqueStringSlice(append(form.filesToDelete, oldNames...))
// slice to fill only with the non-deleted indexes
nonDeleted := make([]string, 0, len(oldNames))
for i, name := range oldNames {
// not marked for deletion
if !list.ExistInSlice(i, indexesToDelete) {
nonDeleted = append(nonDeleted, name)
continue
}
form.filesToUpload = append(form.filesToUpload, files[0])
form.Data[key] = files[0].Name()
} else if options.MaxSelect > 1 {
// append the id of each uploaded file instance
form.filesToUpload = append(form.filesToUpload, files...)
for _, file := range files {
oldNames = append(oldNames, file.Name())
}
form.Data[key] = oldNames
// store the id to actually delete the file later
form.filesToDelete = append(form.filesToDelete, name)
}
} else {
form.Data[key] = value
form.Data[key] = nonDeleted
}
// check if there are any new uploaded form files
files, err := rest.FindUploadedFiles(r, key)
if err != nil {
continue // skip invalid or missing file(s)
}
// refresh oldNames list
oldNames = list.ToUniqueStringSlice(form.Data[key])
if options.MaxSelect == 1 {
// delete previous file(s) before replacing
if len(oldNames) > 0 {
form.filesToDelete = list.ToUniqueStringSlice(append(form.filesToDelete, oldNames...))
}
form.filesToUpload = append(form.filesToUpload, files[0])
form.Data[key] = files[0].Name()
} else if options.MaxSelect > 1 {
// append the id of each uploaded file instance
form.filesToUpload = append(form.filesToUpload, files...)
for _, file := range files {
oldNames = append(oldNames, file.Name())
}
form.Data[key] = oldNames
}
}
@@ -271,7 +272,10 @@ func (form *RecordUpsert) DrySubmit(callback func(txDao *daos.Dao) error) error
}
// Submit validates the form and upserts the form Record model.
func (form *RecordUpsert) Submit() error {
//
// You can optionally provide a list of InterceptorFunc to further
// modify the form behavior before persisting it.
func (form *RecordUpsert) Submit(interceptors ...InterceptorFunc) error {
if err := form.Validate(); err != nil {
return err
}
@@ -281,25 +285,27 @@ func (form *RecordUpsert) Submit() error {
return err
}
return form.app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
// persist record model
if err := txDao.SaveRecord(form.record); err != nil {
return err
}
return runInterceptors(func() error {
return form.app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
// persist record model
if err := txDao.SaveRecord(form.record); err != nil {
return err
}
// upload new files (if any)
if err := form.processFilesToUpload(); err != nil {
return err
}
// upload new files (if any)
if err := form.processFilesToUpload(); err != nil {
return err
}
// delete old files (if any)
if err := form.processFilesToDelete(); err != nil {
// for now fail silently to avoid reupload when `form.Submit()`
// is called manually (aka. not from an api request)...
}
// delete old files (if any)
if err := form.processFilesToDelete(); err != nil { //nolint:staticcheck
// for now fail silently to avoid reupload when `form.Submit()`
// is called manually (aka. not from an api request)...
}
return nil
})
return nil
})
}, interceptors...)
}
func (form *RecordUpsert) processFilesToUpload() error {
@@ -317,17 +323,22 @@ func (form *RecordUpsert) processFilesToUpload() error {
}
defer fs.Close()
var uploadErrors []error
for i := len(form.filesToUpload) - 1; i >= 0; i-- {
file := form.filesToUpload[i]
path := form.record.BaseFilesPath() + "/" + file.Name()
if err := fs.Upload(file.Bytes(), path); err == nil {
// remove the uploaded file from the list
form.filesToUpload = append(form.filesToUpload[:i], form.filesToUpload[i+1:]...)
} else {
// store the upload error
uploadErrors = append(uploadErrors, fmt.Errorf("File %d: %v", i, err))
}
}
if len(form.filesToUpload) > 0 {
return errors.New("Failed to upload all files.")
if len(uploadErrors) > 0 {
return fmt.Errorf("Failed to upload all files: %v", uploadErrors)
}
return nil
@@ -348,20 +359,25 @@ func (form *RecordUpsert) processFilesToDelete() error {
}
defer fs.Close()
var deleteErrors []error
for i := len(form.filesToDelete) - 1; i >= 0; i-- {
filename := form.filesToDelete[i]
path := form.record.BaseFilesPath() + "/" + filename
if err := fs.Delete(path); err == nil {
// remove the deleted file from the list
form.filesToDelete = append(form.filesToDelete[:i], form.filesToDelete[i+1:]...)
} else {
// store the delete error
deleteErrors = append(deleteErrors, fmt.Errorf("File %d: %v", i, err))
}
// try to delete the related file thumbs (if any)
fs.DeletePrefix(form.record.BaseFilesPath() + "/thumbs_" + filename + "/")
}
if len(form.filesToDelete) > 0 {
return errors.New("Failed to delete all files.")
if len(deleteErrors) > 0 {
return fmt.Errorf("Failed to delete all files: %v", deleteErrors)
}
return nil
+87 -7
View File
@@ -3,6 +3,7 @@ package forms_test
import (
"bytes"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"path/filepath"
@@ -29,7 +30,7 @@ func TestNewRecordUpsert(t *testing.T) {
form := forms.NewRecordUpsert(app, record)
val, _ := form.Data["title"]
val := form.Data["title"]
if val != "test_value" {
t.Errorf("Expected record data to be load, got %v", form.Data)
}
@@ -97,8 +98,8 @@ func TestRecordUpsertLoadDataJson(t *testing.T) {
if !ok {
t.Fatal("Expect onefile field to be set")
}
if onefile != nil {
t.Fatalf("Expect onefile field to be nil, got %v", onefile)
if onefile != "" {
t.Fatalf("Expect onefile field to be empty string, got %v", onefile)
}
manyfiles, ok := form.Data["manyfiles"]
@@ -165,8 +166,8 @@ func TestRecordUpsertLoadDataMultipart(t *testing.T) {
if !ok {
t.Fatal("Expect onefile field to be set")
}
if onefile != nil {
t.Fatalf("Expect onefile field to be nil, got %v", onefile)
if onefile != "" {
t.Fatalf("Expect onefile field to be empty string, got %v", onefile)
}
manyfiles, ok := form.Data["manyfiles"]
@@ -400,13 +401,27 @@ func TestRecordUpsertSubmitFailure(t *testing.T) {
req.Header.Set(echo.HeaderContentType, mp.FormDataContentType())
form.LoadData(req)
interceptorCalls := 0
interceptor := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
interceptorCalls++
return next()
}
}
// ensure that validate is triggered
// ---
result := form.Submit()
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().FindFirstRecordByData(collection, "id", recordBefore.Id)
@@ -451,11 +466,25 @@ func TestRecordUpsertSubmitSuccess(t *testing.T) {
req.Header.Set(echo.HeaderContentType, mp.FormDataContentType())
form.LoadData(req)
result := form.Submit()
interceptorCalls := 0
interceptor := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
interceptorCalls++
return next()
}
}
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().FindFirstRecordByData(collection, "id", recordBefore.Id)
@@ -482,6 +511,57 @@ func TestRecordUpsertSubmitSuccess(t *testing.T) {
}
}
func TestRecordUpsertSubmitInterceptors(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
collection, _ := app.Dao().FindCollectionByNameOrId("demo4")
record, err := app.Dao().FindFirstRecordByData(collection, "id", "054f9f24-0a0a-4e09-87b1-bc7ff2b336a2")
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) forms.InterceptorNextFunc {
return func() error {
interceptor1Called = true
return next()
}
}
interceptor2Called := false
interceptor2 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
interceptorRecordTitle = record.GetStringDataValue("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 hasRecordFile(app core.App, record *models.Record, filename string) bool {
fs, _ := app.NewFilesystem()
defer fs.Close()
+20 -15
View File
@@ -33,27 +33,32 @@ func (form *SettingsUpsert) Validate() error {
// Submit validates the form and upserts the loaded settings.
//
// On success the app settings will be refreshed with the form ones.
func (form *SettingsUpsert) Submit() error {
//
// You can optionally provide a list of InterceptorFunc to further
// modify the form behavior before persisting it.
func (form *SettingsUpsert) Submit(interceptors ...InterceptorFunc) error {
if err := form.Validate(); err != nil {
return err
}
encryptionKey := os.Getenv(form.app.EncryptionEnv())
saveErr := form.app.Dao().SaveParam(
models.ParamAppSettings,
form.Settings,
encryptionKey,
)
if saveErr != nil {
return saveErr
}
return runInterceptors(func() error {
saveErr := form.app.Dao().SaveParam(
models.ParamAppSettings,
form.Settings,
encryptionKey,
)
if saveErr != nil {
return saveErr
}
// explicitly trigger old logs deletion
form.app.LogsDao().DeleteOldRequests(
time.Now().AddDate(0, 0, -1*form.Settings.Logs.MaxDays),
)
// explicitly trigger old logs deletion
form.app.LogsDao().DeleteOldRequests(
time.Now().AddDate(0, 0, -1*form.Settings.Logs.MaxDays),
)
// merge the application settings with the form ones
return form.app.Settings().Merge(form.Settings)
// merge the application settings with the form ones
return form.app.Settings().Merge(form.Settings)
}, interceptors...)
}
+58 -1
View File
@@ -2,6 +2,7 @@ package forms_test
import (
"encoding/json"
"errors"
"os"
"testing"
@@ -98,14 +99,31 @@ func TestSettingsUpsertSubmit(t *testing.T) {
continue
}
interceptorCalls := 0
interceptor := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
interceptorCalls++
return next()
}
}
// parse errors
result := form.Submit()
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)
@@ -128,3 +146,42 @@ func TestSettingsUpsertSubmit(t *testing.T) {
}
}
}
func TestSettingsUpsertSubmitInterceptors(t *testing.T) {
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) forms.InterceptorNextFunc {
return func() error {
interceptor1Called = true
return next()
}
}
interceptor2Called := false
interceptor2 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() 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")
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"github.com/pocketbase/pocketbase/models"
)
// UserEmailChangeConfirm defines a user email change request form.
// UserEmailChangeRequest defines a user email change request form.
type UserEmailChangeRequest struct {
app core.App
user *models.User
+26 -25
View File
@@ -68,7 +68,7 @@ func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) {
return nil, nil, err
}
config, _ := form.app.Settings().NamedAuthProviderConfigs()[form.Provider]
config := form.app.Settings().NamedAuthProviderConfigs()[form.Provider]
config.SetupProvider(provider)
provider.SetRedirectUrl(form.RedirectUrl)
@@ -98,35 +98,36 @@ func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) {
return nil, authData, err
}
}
} else {
if !config.AllowRegistrations {
// registration of new users is not allowed via the Oauth2 provider
return nil, authData, errors.New("Cannot find user with the authorized email.")
}
return user, authData, nil
}
// create new user
user = &models.User{Verified: true}
upsertForm := NewUserUpsert(form.app, user)
upsertForm.Email = authData.Email
upsertForm.Password = security.RandomString(30)
upsertForm.PasswordConfirm = upsertForm.Password
if !config.AllowRegistrations {
// registration of new users is not allowed via the Oauth2 provider
return nil, authData, errors.New("Cannot find user with the authorized email.")
}
event := &core.UserOauth2RegisterEvent{
User: user,
AuthData: authData,
}
// create new user
user = &models.User{Verified: true}
upsertForm := NewUserUpsert(form.app, user)
upsertForm.Email = authData.Email
upsertForm.Password = security.RandomString(30)
upsertForm.PasswordConfirm = upsertForm.Password
if err := form.app.OnUserBeforeOauth2Register().Trigger(event); err != nil {
return nil, authData, err
}
event := &core.UserOauth2RegisterEvent{
User: user,
AuthData: authData,
}
if err := upsertForm.Submit(); err != nil {
return nil, authData, err
}
if err := form.app.OnUserBeforeOauth2Register().Trigger(event); err != nil {
return nil, authData, err
}
if err := form.app.OnUserAfterOauth2Register().Trigger(event); err != nil {
return nil, authData, err
}
if err := upsertForm.Submit(); err != nil {
return nil, authData, err
}
if err := form.app.OnUserAfterOauth2Register().Trigger(event); err != nil {
return nil, authData, err
}
return user, authData, nil
+1 -1
View File
@@ -148,7 +148,7 @@ func TestUserPasswordResetConfirmSubmit(t *testing.T) {
}
claims, _ := security.ParseUnverifiedJWT(form.Token)
tokenUserId, _ := claims["id"]
tokenUserId := claims["id"]
if user.Id != tokenUserId {
t.Errorf("(%d) Expected user with id %s, got %v", i, tokenUserId, user)
+7 -2
View File
@@ -98,7 +98,10 @@ func (form *UserUpsert) checkEmailDomain(value any) error {
}
// Submit validates the form and upserts the form user model.
func (form *UserUpsert) Submit() error {
//
// You can optionally provide a list of InterceptorFunc to further
// modify the form behavior before persisting it.
func (form *UserUpsert) Submit(interceptors ...InterceptorFunc) error {
if err := form.Validate(); err != nil {
return err
}
@@ -114,5 +117,7 @@ func (form *UserUpsert) Submit() error {
form.user.Email = form.Email
return form.app.Dao().SaveUser(form.user)
return runInterceptors(func() error {
return form.app.Dao().SaveUser(form.user)
}, interceptors...)
}
+65 -1
View File
@@ -2,6 +2,7 @@ package forms_test
import (
"encoding/json"
"errors"
"testing"
validation "github.com/go-ozzo/ozzo-validation/v4"
@@ -212,13 +213,28 @@ func TestUserUpsertSubmit(t *testing.T) {
continue
}
err := form.Submit()
interceptorCalls := 0
err := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
interceptorCalls++
return next()
}
})
hasErr := err != nil
if hasErr != s.expectError {
t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err)
}
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
}
@@ -240,3 +256,51 @@ func TestUserUpsertSubmit(t *testing.T) {
}
}
}
func TestUserUpsertSubmitInterceptors(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
user := &models.User{}
form := forms.NewUserUpsert(app, user)
form.Email = "test_new@example.com"
form.Password = "1234567890"
form.PasswordConfirm = form.Password
testErr := errors.New("test_error")
interceptorUserEmail := ""
interceptor1Called := false
interceptor1 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
interceptor1Called = true
return next()
}
}
interceptor2Called := false
interceptor2 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
interceptorUserEmail = user.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 interceptorUserEmail != form.Email {
t.Fatalf("Expected the form model to be filled before calling the interceptors")
}
}
+1 -1
View File
@@ -127,7 +127,7 @@ func TestUserVerificationConfirmSubmit(t *testing.T) {
}
claims, _ := security.ParseUnverifiedJWT(form.Token)
tokenUserId, _ := claims["id"]
tokenUserId := claims["id"]
if user.Id != tokenUserId {
t.Errorf("(%d) Expected user.Id %q, got %q", i, tokenUserId, user.Id)
+19 -33
View File
@@ -6,9 +6,9 @@ import (
"regexp"
"strings"
"github.com/pocketbase/dbx"
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"
@@ -240,7 +240,6 @@ func (validator *RecordDataValidator) checkUrlValue(field *schema.SchemaField, v
func (validator *RecordDataValidator) checkDateValue(field *schema.SchemaField, value any) error {
val, _ := value.(types.DateTime)
if val.IsZero() {
if field.Required {
return requiredErr
@@ -268,6 +267,9 @@ func (validator *RecordDataValidator) checkDateValue(field *schema.SchemaField,
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
}
@@ -302,13 +304,9 @@ func (validator *RecordDataValidator) checkJsonValue(field *schema.SchemaField,
}
func (validator *RecordDataValidator) checkFileValue(field *schema.SchemaField, value any) error {
// normalize value access
var names []string
switch v := value.(type) {
case []string:
names = v
case string:
names = []string{v}
names := list.ToUniqueStringSlice(value)
if len(names) == 0 && field.Required {
return requiredErr
}
options, _ := field.Options.(*schema.FileOptions)
@@ -318,12 +316,10 @@ func (validator *RecordDataValidator) checkFileValue(field *schema.SchemaField,
}
// extract the uploaded files
files := []*rest.UploadedFile{}
if len(validator.uploadedFiles) > 0 {
for _, file := range validator.uploadedFiles {
if list.ExistInSlice(file.Name(), names) {
files = append(files, file)
}
files := make([]*rest.UploadedFile, 0, len(validator.uploadedFiles))
for _, file := range validator.uploadedFiles {
if list.ExistInSlice(file.Name(), names) {
files = append(files, file)
}
}
@@ -345,16 +341,11 @@ func (validator *RecordDataValidator) checkFileValue(field *schema.SchemaField,
}
func (validator *RecordDataValidator) checkRelationValue(field *schema.SchemaField, value any) error {
// normalize value access
var ids []string
switch v := value.(type) {
case []string:
ids = v
case string:
ids = []string{v}
}
ids := list.ToUniqueStringSlice(value)
if len(ids) == 0 {
if field.Required {
return requiredErr
}
return nil // nothing to check
}
@@ -385,16 +376,11 @@ func (validator *RecordDataValidator) checkRelationValue(field *schema.SchemaFie
}
func (validator *RecordDataValidator) checkUserValue(field *schema.SchemaField, value any) error {
// normalize value access
var ids []string
switch v := value.(type) {
case []string:
ids = v
case string:
ids = []string{v}
}
ids := list.ToUniqueStringSlice(value)
if len(ids) == 0 {
if field.Required {
return requiredErr
}
return nil // nothing to check
}
+63 -63
View File
@@ -89,7 +89,7 @@ func TestRecordDataValidatorValidateText(t *testing.T) {
scenarios := []testDataFieldScenario{
{
"check required constraint",
"(text) check required constraint",
map[string]any{
"field1": nil,
"field2": nil,
@@ -99,7 +99,7 @@ func TestRecordDataValidatorValidateText(t *testing.T) {
[]string{"field2"},
},
{
"check unique constraint",
"(text) check unique constraint",
map[string]any{
"field1": "test",
"field2": "test",
@@ -109,7 +109,7 @@ func TestRecordDataValidatorValidateText(t *testing.T) {
[]string{"field3"},
},
{
"check min constraint",
"(text) check min constraint",
map[string]any{
"field1": "test",
"field2": "test",
@@ -119,7 +119,7 @@ func TestRecordDataValidatorValidateText(t *testing.T) {
[]string{"field3"},
},
{
"check max constraint",
"(text) check max constraint",
map[string]any{
"field1": "test",
"field2": "test",
@@ -129,7 +129,7 @@ func TestRecordDataValidatorValidateText(t *testing.T) {
[]string{"field3"},
},
{
"check pattern constraint",
"(text) check pattern constraint",
map[string]any{
"field1": nil,
"field2": "test",
@@ -139,7 +139,7 @@ func TestRecordDataValidatorValidateText(t *testing.T) {
[]string{"field3"},
},
{
"valid data (only required)",
"(text) valid data (only required)",
map[string]any{
"field2": "test",
},
@@ -147,7 +147,7 @@ func TestRecordDataValidatorValidateText(t *testing.T) {
[]string{},
},
{
"valid data (all)",
"(text) valid data (all)",
map[string]any{
"field1": "test",
"field2": 12345, // test value cast
@@ -168,7 +168,7 @@ func TestRecordDataValidatorValidateNumber(t *testing.T) {
// create new test collection
collection := &models.Collection{}
collection.Name = "validate_test"
min := 1.0
min := 0.0
max := 150.0
collection.Schema = schema.NewSchema(
&schema.SchemaField{
@@ -205,7 +205,7 @@ func TestRecordDataValidatorValidateNumber(t *testing.T) {
scenarios := []testDataFieldScenario{
{
"check required constraint",
"(number) check required constraint",
map[string]any{
"field1": nil,
"field2": nil,
@@ -215,17 +215,17 @@ func TestRecordDataValidatorValidateNumber(t *testing.T) {
[]string{"field2"},
},
{
"check required constraint + casting",
"(number) check required constraint + casting",
map[string]any{
"field1": "invalid",
"field2": "invalid",
"field3": "invalid",
},
nil,
[]string{"field2", "field3"},
[]string{"field2"},
},
{
"check unique constraint",
"(number) check unique constraint",
map[string]any{
"field1": 123,
"field2": 123,
@@ -235,7 +235,7 @@ func TestRecordDataValidatorValidateNumber(t *testing.T) {
[]string{"field3"},
},
{
"check min constraint",
"(number) check min constraint",
map[string]any{
"field1": 0.5,
"field2": 1,
@@ -245,7 +245,7 @@ func TestRecordDataValidatorValidateNumber(t *testing.T) {
[]string{"field3"},
},
{
"check max constraint",
"(number) check max constraint",
map[string]any{
"field1": nil,
"field2": max,
@@ -255,7 +255,7 @@ func TestRecordDataValidatorValidateNumber(t *testing.T) {
[]string{"field3"},
},
{
"valid data (only required)",
"(number) valid data (only required)",
map[string]any{
"field2": 1,
},
@@ -263,7 +263,7 @@ func TestRecordDataValidatorValidateNumber(t *testing.T) {
[]string{},
},
{
"valid data (all)",
"(number) valid data (all)",
map[string]any{
"field1": nil,
"field2": 123, // test value cast
@@ -316,7 +316,7 @@ func TestRecordDataValidatorValidateBool(t *testing.T) {
scenarios := []testDataFieldScenario{
{
"check required constraint",
"(bool) check required constraint",
map[string]any{
"field1": nil,
"field2": nil,
@@ -326,7 +326,7 @@ func TestRecordDataValidatorValidateBool(t *testing.T) {
[]string{"field2"},
},
{
"check required constraint + casting",
"(bool) check required constraint + casting",
map[string]any{
"field1": "invalid",
"field2": "invalid",
@@ -336,7 +336,7 @@ func TestRecordDataValidatorValidateBool(t *testing.T) {
[]string{"field2"},
},
{
"check unique constraint",
"(bool) check unique constraint",
map[string]any{
"field1": true,
"field2": true,
@@ -346,7 +346,7 @@ func TestRecordDataValidatorValidateBool(t *testing.T) {
[]string{"field3"},
},
{
"valid data (only required)",
"(bool) valid data (only required)",
map[string]any{
"field2": 1,
},
@@ -354,7 +354,7 @@ func TestRecordDataValidatorValidateBool(t *testing.T) {
[]string{},
},
{
"valid data (all)",
"(bool) valid data (all)",
map[string]any{
"field1": false,
"field2": true,
@@ -412,7 +412,7 @@ func TestRecordDataValidatorValidateEmail(t *testing.T) {
scenarios := []testDataFieldScenario{
{
"check required constraint",
"(email) check required constraint",
map[string]any{
"field1": nil,
"field2": nil,
@@ -422,7 +422,7 @@ func TestRecordDataValidatorValidateEmail(t *testing.T) {
[]string{"field2"},
},
{
"check email format validator",
"(email) check email format validator",
map[string]any{
"field1": "test",
"field2": "test.com",
@@ -432,7 +432,7 @@ func TestRecordDataValidatorValidateEmail(t *testing.T) {
[]string{"field1", "field2", "field3"},
},
{
"check unique constraint",
"(email) check unique constraint",
map[string]any{
"field1": "test@example.com",
"field2": "test@test.com",
@@ -442,7 +442,7 @@ func TestRecordDataValidatorValidateEmail(t *testing.T) {
[]string{"field3"},
},
{
"check ExceptDomains constraint",
"(email) check ExceptDomains constraint",
map[string]any{
"field1": "test@example.com",
"field2": "test@example.com",
@@ -452,7 +452,7 @@ func TestRecordDataValidatorValidateEmail(t *testing.T) {
[]string{"field2"},
},
{
"check OnlyDomains constraint",
"(email) check OnlyDomains constraint",
map[string]any{
"field1": "test@test.com",
"field2": "test@test.com",
@@ -462,7 +462,7 @@ func TestRecordDataValidatorValidateEmail(t *testing.T) {
[]string{"field3"},
},
{
"valid data (only required)",
"(email) valid data (only required)",
map[string]any{
"field2": "test@test.com",
},
@@ -470,7 +470,7 @@ func TestRecordDataValidatorValidateEmail(t *testing.T) {
[]string{},
},
{
"valid data (all)",
"(email) valid data (all)",
map[string]any{
"field1": "123@example.com",
"field2": "test@test.com",
@@ -528,7 +528,7 @@ func TestRecordDataValidatorValidateUrl(t *testing.T) {
scenarios := []testDataFieldScenario{
{
"check required constraint",
"(url) check required constraint",
map[string]any{
"field1": nil,
"field2": nil,
@@ -538,7 +538,7 @@ func TestRecordDataValidatorValidateUrl(t *testing.T) {
[]string{"field2"},
},
{
"check url format validator",
"(url) check url format validator",
map[string]any{
"field1": "/abc",
"field2": "test.com", // valid
@@ -548,7 +548,7 @@ func TestRecordDataValidatorValidateUrl(t *testing.T) {
[]string{"field1", "field3"},
},
{
"check unique constraint",
"(url) check unique constraint",
map[string]any{
"field1": "http://example.com",
"field2": "http://test.com",
@@ -558,7 +558,7 @@ func TestRecordDataValidatorValidateUrl(t *testing.T) {
[]string{"field3"},
},
{
"check ExceptDomains constraint",
"(url) check ExceptDomains constraint",
map[string]any{
"field1": "http://example.com",
"field2": "http://example.com",
@@ -568,7 +568,7 @@ func TestRecordDataValidatorValidateUrl(t *testing.T) {
[]string{"field2"},
},
{
"check OnlyDomains constraint",
"(url) check OnlyDomains constraint",
map[string]any{
"field1": "http://test.com/abc",
"field2": "http://test.com/abc",
@@ -578,7 +578,7 @@ func TestRecordDataValidatorValidateUrl(t *testing.T) {
[]string{"field3"},
},
{
"check subdomains constraint",
"(url) check subdomains constraint",
map[string]any{
"field1": "http://test.test.com",
"field2": "http://test.example.com",
@@ -588,7 +588,7 @@ func TestRecordDataValidatorValidateUrl(t *testing.T) {
[]string{"field3"},
},
{
"valid data (only required)",
"(url) valid data (only required)",
map[string]any{
"field2": "http://sub.test.com/abc",
},
@@ -596,7 +596,7 @@ func TestRecordDataValidatorValidateUrl(t *testing.T) {
[]string{},
},
{
"valid data (all)",
"(url) valid data (all)",
map[string]any{
"field1": "http://example.com/123",
"field2": "http://test.com/",
@@ -656,7 +656,7 @@ func TestRecordDataValidatorValidateDate(t *testing.T) {
scenarios := []testDataFieldScenario{
{
"check required constraint",
"(date) check required constraint",
map[string]any{
"field1": nil,
"field2": nil,
@@ -666,7 +666,7 @@ func TestRecordDataValidatorValidateDate(t *testing.T) {
[]string{"field2"},
},
{
"check required constraint + cast",
"(date) check required constraint + cast",
map[string]any{
"field1": "invalid",
"field2": "invalid",
@@ -676,7 +676,7 @@ func TestRecordDataValidatorValidateDate(t *testing.T) {
[]string{"field2"},
},
{
"check required constraint + zero datetime",
"(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",
@@ -686,7 +686,7 @@ func TestRecordDataValidatorValidateDate(t *testing.T) {
[]string{"field2"},
},
{
"check unique constraint",
"(date) check unique constraint",
map[string]any{
"field1": "2029-01-01 01:01:01.123",
"field2": "2029-01-01 01:01:01.123",
@@ -696,7 +696,7 @@ func TestRecordDataValidatorValidateDate(t *testing.T) {
[]string{"field3"},
},
{
"check min date constraint",
"(date) check min date constraint",
map[string]any{
"field1": "2021-01-01 01:01:01",
"field2": "2021-01-01 01:01:01",
@@ -706,7 +706,7 @@ func TestRecordDataValidatorValidateDate(t *testing.T) {
[]string{"field2"},
},
{
"check max date constraint",
"(date) check max date constraint",
map[string]any{
"field1": "2030-02-01 01:01:01",
"field2": "2030-02-01 01:01:01",
@@ -716,7 +716,7 @@ func TestRecordDataValidatorValidateDate(t *testing.T) {
[]string{"field3"},
},
{
"valid data (only required)",
"(date) valid data (only required)",
map[string]any{
"field2": "2029-01-01 01:01:01",
},
@@ -724,7 +724,7 @@ func TestRecordDataValidatorValidateDate(t *testing.T) {
[]string{},
},
{
"valid data (all)",
"(date) valid data (all)",
map[string]any{
"field1": "2029-01-01 01:01:01.000",
"field2": "2029-01-01 01:01:01",
@@ -788,7 +788,7 @@ func TestRecordDataValidatorValidateSelect(t *testing.T) {
scenarios := []testDataFieldScenario{
{
"check required constraint",
"(select) check required constraint",
map[string]any{
"field1": nil,
"field2": nil,
@@ -798,7 +798,7 @@ func TestRecordDataValidatorValidateSelect(t *testing.T) {
[]string{"field2"},
},
{
"check required constraint - empty values",
"(select) check required constraint - empty values",
map[string]any{
"field1": "",
"field2": "",
@@ -808,7 +808,7 @@ func TestRecordDataValidatorValidateSelect(t *testing.T) {
[]string{"field2"},
},
{
"check required constraint - multiple select cast",
"(select) check required constraint - multiple select cast",
map[string]any{
"field1": "a",
"field2": "a",
@@ -818,7 +818,7 @@ func TestRecordDataValidatorValidateSelect(t *testing.T) {
[]string{},
},
{
"check unique constraint",
"(select) check unique constraint",
map[string]any{
"field1": "a",
"field2": "b",
@@ -828,7 +828,7 @@ func TestRecordDataValidatorValidateSelect(t *testing.T) {
[]string{"field3"},
},
{
"check unique constraint - same elements but different order",
"(select) check unique constraint - same elements but different order",
map[string]any{
"field1": "a",
"field2": "b",
@@ -838,7 +838,7 @@ func TestRecordDataValidatorValidateSelect(t *testing.T) {
[]string{},
},
{
"check Values constraint",
"(select) check Values constraint",
map[string]any{
"field1": 1,
"field2": "d",
@@ -848,7 +848,7 @@ func TestRecordDataValidatorValidateSelect(t *testing.T) {
[]string{"field2", "field3"},
},
{
"check MaxSelect constraint",
"(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"},
@@ -858,7 +858,7 @@ func TestRecordDataValidatorValidateSelect(t *testing.T) {
[]string{"field2"},
},
{
"valid data - only required fields",
"(select) valid data - only required fields",
map[string]any{
"field2": []string{"a", "b"},
},
@@ -866,7 +866,7 @@ func TestRecordDataValidatorValidateSelect(t *testing.T) {
[]string{},
},
{
"valid data - all fields with normalizations",
"(select) valid data - all fields with normalizations",
map[string]any{
"field1": "a",
"field2": []string{"a", "b", "b"}, // will be collapsed
@@ -918,7 +918,7 @@ func TestRecordDataValidatorValidateJson(t *testing.T) {
scenarios := []testDataFieldScenario{
{
"check required constraint - nil",
"(json) check required constraint - nil",
map[string]any{
"field1": nil,
"field2": nil,
@@ -928,7 +928,7 @@ func TestRecordDataValidatorValidateJson(t *testing.T) {
[]string{"field2"},
},
{
"check required constraint - zero string",
"(json) check required constraint - zero string",
map[string]any{
"field1": "",
"field2": "",
@@ -938,7 +938,7 @@ func TestRecordDataValidatorValidateJson(t *testing.T) {
[]string{"field2"},
},
{
"check required constraint - zero number",
"(json) check required constraint - zero number",
map[string]any{
"field1": 0,
"field2": 0,
@@ -948,7 +948,7 @@ func TestRecordDataValidatorValidateJson(t *testing.T) {
[]string{},
},
{
"check required constraint - zero slice",
"(json) check required constraint - zero slice",
map[string]any{
"field1": []string{},
"field2": []string{},
@@ -958,7 +958,7 @@ func TestRecordDataValidatorValidateJson(t *testing.T) {
[]string{},
},
{
"check required constraint - zero map",
"(json) check required constraint - zero map",
map[string]any{
"field1": map[string]string{},
"field2": map[string]string{},
@@ -968,7 +968,7 @@ func TestRecordDataValidatorValidateJson(t *testing.T) {
[]string{},
},
{
"check unique constraint",
"(json) check unique constraint",
map[string]any{
"field1": `{"test":123}`,
"field2": `{"test":123}`,
@@ -978,7 +978,7 @@ func TestRecordDataValidatorValidateJson(t *testing.T) {
[]string{"field3"},
},
{
"check json text validator",
"(json) check json text validator",
map[string]any{
"field1": `[1, 2, 3`,
"field2": `invalid`,
@@ -988,7 +988,7 @@ func TestRecordDataValidatorValidateJson(t *testing.T) {
[]string{"field1", "field2"},
},
{
"valid data - only required fields",
"(json) valid data - only required fields",
map[string]any{
"field2": `{"test":123}`,
},
@@ -996,7 +996,7 @@ func TestRecordDataValidatorValidateJson(t *testing.T) {
[]string{},
},
{
"valid data - all fields with normalizations",
"(json) valid data - all fields with normalizations",
map[string]any{
"field1": []string{"a", "b", "c"},
"field2": 123,
@@ -1189,7 +1189,7 @@ func TestRecordDataValidatorValidateRelation(t *testing.T) {
Type: schema.FieldTypeRelation,
Options: &schema.RelationOptions{
MaxSelect: 3,
CollectionId: "", // missing or nonexisting colleciton id
CollectionId: "", // missing or non-existing collection id
},
},
)
+30
View File
@@ -0,0 +1,30 @@
run:
go: 1.18
concurrency: 4
timeout: 10m
linters:
disable-all: true
enable:
- asciicheck
- deadcode
- depguard
- exportloopref
- gocritic
- gofmt
- goimports
- gomodguard
- goprintffuncname
- gosimple
- govet
- ineffassign
- misspell
- nakedret
- nolintlint
- prealloc
- staticcheck
- typecheck
- unconvert
- unused
- varcheck
- whitespace
+10 -1
View File
@@ -3,6 +3,8 @@ package migrations
import (
"fmt"
"path/filepath"
"runtime"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
@@ -20,7 +22,14 @@ func Register(
down func(db dbx.Builder) error,
optFilename ...string,
) {
AppMigrations.Register(up, down, optFilename...)
var optFiles []string
if len(optFilename) > 0 {
optFiles = optFilename
} else {
_, path, _, _ := runtime.Caller(1)
optFiles = append(optFiles, filepath.Base(path))
}
AppMigrations.Register(up, down, optFiles...)
}
func init() {
+8 -2
View File
@@ -15,6 +15,12 @@ type ColumnValueMapper interface {
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
@@ -53,7 +59,7 @@ func (m *BaseModel) GetCreated() types.DateTime {
return m.Created
}
// GetCreated returns the model's Updated datetime.
// GetUpdated returns the model's Updated datetime.
func (m *BaseModel) GetUpdated() types.DateTime {
return m.Updated
}
@@ -71,7 +77,7 @@ func (m *BaseModel) RefreshCreated() {
m.Created = types.NowDateTime()
}
// RefreshCreated updates the model's Created field with the current datetime.
// RefreshUpdated updates the model's Updated field with the current datetime.
func (m *BaseModel) RefreshUpdated() {
m.Updated = types.NowDateTime()
}
+1
View File
@@ -3,6 +3,7 @@ package models
import "github.com/pocketbase/pocketbase/models/schema"
var _ Model = (*Collection)(nil)
var _ FilesManager = (*Collection)(nil)
type Collection struct {
BaseModel
+6 -5
View File
@@ -16,6 +16,7 @@ import (
var _ Model = (*Record)(nil)
var _ ColumnValueMapper = (*Record)(nil)
var _ FilesManager = (*Record)(nil)
type Record struct {
BaseModel
@@ -68,21 +69,21 @@ func NewRecordFromNullStringMap(collection *Collection, data dbx.NullStringMap)
// NewRecordsFromNullStringMaps initializes a new Record model for
// each row in the provided NullStringMap slice.
func NewRecordsFromNullStringMaps(collection *Collection, rows []dbx.NullStringMap) []*Record {
result := []*Record{}
result := make([]*Record, len(rows))
for _, row := range rows {
result = append(result, NewRecordFromNullStringMap(collection, row))
for i, row := range rows {
result[i] = NewRecordFromNullStringMap(collection, row)
}
return result
}
// Returns the table name associated to the current Record model.
// TableName returns the table name associated to the current Record model.
func (m *Record) TableName() string {
return m.collection.Name
}
// Returns the Collection model associated to the current Record model.
// Collection returns the Collection model associated to the current Record model.
func (m *Record) Collection() *Collection {
return m.collection
}
+3 -3
View File
@@ -118,7 +118,7 @@ func TestNewRecordFromNullStringMap(t *testing.T) {
t.Fatal(err)
}
expected := `{"@collectionId":"","@collectionName":"test","created":"2022-01-01 10:00:00.123","field1":"test","field2":null,"field3":true,"field4":123.123,"field5":"test1","field6":["test"],"id":"c23eb053-d07e-4fbe-86b3-b8ac31982e9a","updated":"2022-01-01 10:00:00.456"}`
expected := `{"@collectionId":"","@collectionName":"test","created":"2022-01-01 10:00:00.123","field1":"test","field2":"","field3":true,"field4":123.123,"field5":"test1","field6":["test"],"id":"c23eb053-d07e-4fbe-86b3-b8ac31982e9a","updated":"2022-01-01 10:00:00.456"}`
if string(encoded) != expected {
t.Fatalf("Expected %v, got \n%v", expected, string(encoded))
@@ -185,10 +185,10 @@ func TestNewRecordsFromNullStringMaps(t *testing.T) {
t.Fatal(err)
}
expected := `[{"@collectionId":"","@collectionName":"test","created":"2022-01-01 10:00:00.123","field1":"test1","field2":null,"id":"11111111-d07e-4fbe-86b3-b8ac31982e9a","updated":"2022-01-01 10:00:00.456"},{"@collectionId":"","@collectionName":"test","created":"","field1":"test2","field2":123,"id":"22222222-d07e-4fbe-86b3-b8ac31982e9a","updated":""}]`
expected := `[{"@collectionId":"","@collectionName":"test","created":"2022-01-01 10:00:00.123","field1":"test1","field2":0,"id":"11111111-d07e-4fbe-86b3-b8ac31982e9a","updated":"2022-01-01 10:00:00.456"},{"@collectionId":"","@collectionName":"test","created":"","field1":"test2","field2":123,"id":"22222222-d07e-4fbe-86b3-b8ac31982e9a","updated":""}]`
if string(encoded) != expected {
t.Fatalf("Expected %v, got \n%v", expected, string(encoded))
t.Fatalf("Expected \n%v, got \n%v", expected, string(encoded))
}
}
+1 -1
View File
@@ -134,7 +134,7 @@ func (s *Schema) AddField(newField *SchemaField) {
// checks for invalid renamed fields and field name duplications.
func (s Schema) Validate() error {
return validation.Validate(&s.fields, validation.Required, validation.By(func(value any) error {
fields := s.fields // use directly the schema value to avoid unnecesary interface casting
fields := s.fields // use directly the schema value to avoid unnecessary interface casting
if len(fields) == 0 {
return validation.NewError("validation_invalid_schema", "Invalid schema format.")
+13 -25
View File
@@ -218,31 +218,19 @@ func (f *SchemaField) PrepareValue(value any) any {
f.InitOptions()
switch f.Type {
case FieldTypeText, FieldTypeEmail, FieldTypeUrl: // string
if value == nil {
return nil
}
case FieldTypeText, FieldTypeEmail, FieldTypeUrl:
return cast.ToString(value)
case FieldTypeJson: // string
if value == nil {
return nil
}
case FieldTypeJson:
val, _ := types.ParseJsonRaw(value)
return val
case FieldTypeNumber: // nil, int or float
if value == nil {
return nil
}
case FieldTypeNumber:
return cast.ToFloat64(value)
case FieldTypeBool: // bool
case FieldTypeBool:
return cast.ToBool(value)
case FieldTypeDate: // string, DateTime or time.Time
if value == nil {
return nil
}
case FieldTypeDate:
val, _ := types.ParseDateTime(value)
return val
case FieldTypeSelect: // nil, string or slice of strings
case FieldTypeSelect:
val := list.ToUniqueStringSlice(value)
options, _ := f.Options.(*SelectOptions)
@@ -250,11 +238,11 @@ func (f *SchemaField) PrepareValue(value any) any {
if len(val) > 0 {
return val[0]
}
return nil
return ""
}
return val
case FieldTypeFile: // nil, string or slice of strings
case FieldTypeFile:
val := list.ToUniqueStringSlice(value)
options, _ := f.Options.(*FileOptions)
@@ -262,11 +250,11 @@ func (f *SchemaField) PrepareValue(value any) any {
if len(val) > 0 {
return val[0]
}
return nil
return ""
}
return val
case FieldTypeRelation: // nil, string or slice of strings
case FieldTypeRelation:
ids := list.ToUniqueStringSlice(value)
options, _ := f.Options.(*RelationOptions)
@@ -274,11 +262,11 @@ func (f *SchemaField) PrepareValue(value any) any {
if len(ids) > 0 {
return ids[0]
}
return nil
return ""
}
return ids
case FieldTypeUser: // nil, string or slice of strings
case FieldTypeUser:
ids := list.ToUniqueStringSlice(value)
options, _ := f.Options.(*UserOptions)
@@ -286,7 +274,7 @@ func (f *SchemaField) PrepareValue(value any) any {
if len(ids) > 0 {
return ids[0]
}
return nil
return ""
}
return ids
+23 -17
View File
@@ -493,24 +493,27 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
value any
expectJson string
}{
{schema.SchemaField{Type: "unkown"}, "test", `"test"`},
{schema.SchemaField{Type: "unkown"}, 123, "123"},
{schema.SchemaField{Type: "unkown"}, []int{1, 2, 1}, "[1,2,1]"},
{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, `null`},
{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, `null`},
{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, `null`},
{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"`},
@@ -523,7 +526,9 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
{schema.SchemaField{Type: schema.FieldTypeJson}, []int{1, 2, 1}, `[1,2,1]`},
// number
{schema.SchemaField{Type: schema.FieldTypeNumber}, nil, "null"},
{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"},
@@ -533,22 +538,24 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
{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, "null"},
{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.000"`},
{schema.SchemaField{Type: schema.FieldTypeDate}, "2022-01-01 11:27:10.123", `"2022-01-01 11:27:10.123"`},
{schema.SchemaField{Type: schema.FieldTypeDate}, types.DateTime{}, `""`},
{schema.SchemaField{Type: schema.FieldTypeDate}, time.Time{}, `""`},
// select (single)
{schema.SchemaField{Type: schema.FieldTypeSelect}, nil, `null`},
{schema.SchemaField{Type: schema.FieldTypeSelect}, "", `null`},
{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"}, `"test1"`},
@@ -624,8 +631,8 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
},
// file (single)
{schema.SchemaField{Type: schema.FieldTypeFile}, nil, `null`},
{schema.SchemaField{Type: schema.FieldTypeFile}, "", `null`},
{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"}, `"test1"`},
@@ -690,8 +697,8 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
},
// relation (single)
{schema.SchemaField{Type: schema.FieldTypeRelation}, nil, `null`},
{schema.SchemaField{Type: schema.FieldTypeRelation}, "", `null`},
{schema.SchemaField{Type: schema.FieldTypeRelation}, nil, `""`},
{schema.SchemaField{Type: schema.FieldTypeRelation}, "", `""`},
{schema.SchemaField{Type: schema.FieldTypeRelation}, 123, `"123"`},
{schema.SchemaField{Type: schema.FieldTypeRelation}, "abc", `"abc"`},
{schema.SchemaField{Type: schema.FieldTypeRelation}, "1ba88b4f-e9da-42f0-9764-9a55c953e724", `"1ba88b4f-e9da-42f0-9764-9a55c953e724"`},
@@ -761,8 +768,8 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
},
// user (single)
{schema.SchemaField{Type: schema.FieldTypeUser}, nil, `null`},
{schema.SchemaField{Type: schema.FieldTypeUser}, "", `null`},
{schema.SchemaField{Type: schema.FieldTypeUser}, nil, `""`},
{schema.SchemaField{Type: schema.FieldTypeUser}, "", `""`},
{schema.SchemaField{Type: schema.FieldTypeUser}, 123, `"123"`},
{schema.SchemaField{Type: schema.FieldTypeUser}, "1ba88b4f-e9da-42f0-9764-9a55c953e724", `"1ba88b4f-e9da-42f0-9764-9a55c953e724"`},
{
@@ -1281,7 +1288,6 @@ func TestFileOptionsValidate(t *testing.T) {
}
func TestRelationOptionsValidate(t *testing.T) {
scenarios := []fieldOptionsScenario{
{
"empty",
+2 -2
View File
@@ -9,10 +9,10 @@ import (
var _ Model = (*User)(nil)
const (
// The name of the system user profiles collection.
// ProfileCollectionName is the name of the system user profiles collection.
ProfileCollectionName = "profiles"
// The name of the user field from the system user profiles collection.
// ProfileCollectionUserFieldName is the name of the user field from the system user profiles collection.
ProfileCollectionUserFieldName = "userId"
)
+8 -7
View File
@@ -17,7 +17,7 @@ import (
var _ core.App = (*PocketBase)(nil)
// Version of PocketBase
const Version = "0.1.0"
var Version = "(untracked)"
// appWrapper serves as a private core.App instance wrapper.
type appWrapper struct {
@@ -73,11 +73,16 @@ func New() *PocketBase {
pb := &PocketBase{
RootCmd: &cobra.Command{
Use: "pocketbase",
Short: "PocketBase CLI",
Use: "pocketbase",
Short: "PocketBase CLI",
Version: Version,
FParseErrWhitelist: cobra.FParseErrWhitelist{
UnknownFlags: true,
},
// no need to provide the default cobra completion command
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: true,
},
},
defaultDebug: withGoRun,
defaultDataDir: defaultDir,
@@ -85,9 +90,6 @@ func New() *PocketBase {
showStartBanner: true,
}
// no need to provide the default cobra completion command
pb.RootCmd.CompletionOptions.DisableDefaultCmd = true
// parse base flags
// (errors are ignored, since the full flags parsing happens on Execute())
pb.eagerParseFlags()
@@ -130,7 +132,6 @@ func (pb *PocketBase) ShowStartBanner(val bool) *PocketBase {
func (pb *PocketBase) Start() error {
// register system commands
pb.RootCmd.AddCommand(cmd.NewServeCommand(pb, pb.showStartBanner))
pb.RootCmd.AddCommand(cmd.NewVersionCommand(pb, Version))
pb.RootCmd.AddCommand(cmd.NewMigrateCommand(pb))
return pb.Execute()
+1 -1
View File
@@ -168,7 +168,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
return "", nil, fmt.Errorf("Failed to find field %q collection.", prop)
}
newCollectionName := relCollection.Name
newTableAlias := (currentTableAlias + "_" + field.Name)
newTableAlias := currentTableAlias + "_" + field.Name
r.addJoin(
newCollectionName,
+2 -2
View File
@@ -48,7 +48,7 @@ func (scenario *ApiScenario) Test(t *testing.T) {
recorder := httptest.NewRecorder()
req := httptest.NewRequest(scenario.Method, scenario.Url, scenario.Body)
// add middeware to timeout long running requests (eg. keep-alive routes)
// 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 {
ctx, cancelFunc := context.WithTimeout(c.Request().Context(), 100*time.Millisecond)
@@ -109,7 +109,7 @@ func (scenario *ApiScenario) Test(t *testing.T) {
}
for event, expectedCalls := range scenario.ExpectedEvents {
actualCalls, _ := testApp.EventCalls[event]
actualCalls := testApp.EventCalls[event]
if actualCalls != expectedCalls {
t.Errorf("[%s] Expected event %s to be called %d, got %d", prefix, event, expectedCalls, actualCalls)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// Pacakge tests provides common helpers and mocks used in PocketBase application tests.
// Package tests provides common helpers and mocks used in PocketBase application tests.
package tests
import (
Binary file not shown.
+1 -1
View File
@@ -29,7 +29,7 @@ type Provider interface {
// SetClientId sets the provider client's ID.
SetClientId(clientId string)
// ClientId returns the provider client's app secret.
// ClientSecret returns the provider client's app secret.
ClientSecret() string
// SetClientSecret sets the provider client's app secret.
+11
View File
@@ -3,6 +3,7 @@ package auth
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
@@ -120,6 +121,16 @@ func (p *baseProvider) FetchRawUserData(token *oauth2.Token, result any) error {
return err
}
// http.Client.Get doesn't treat non 2xx responses as error
if response.StatusCode >= 400 {
return fmt.Errorf(
"Failed to fetch OAuth2 user profile via %s (%d):\n%s",
p.userApiUrl,
response.StatusCode,
string(content),
)
}
return json.Unmarshal(content, &result)
}
+11 -7
View File
@@ -127,7 +127,7 @@ func (s *System) DeletePrefix(prefix string) []error {
Prefix: prefix,
}
// delete all files witht the prefix
// delete all files with the prefix
// ---
iter := s.bucket.List(&opts)
for {
@@ -153,7 +153,7 @@ func (s *System) DeletePrefix(prefix string) []error {
// (this operation usually is optional and there is no need to strictly check the result)
// ---
// fill dirs slice
dirs := []string{}
dirs := make([]string, 0, len(dirsMap))
for d := range dirsMap {
dirs = append(dirs, d)
}
@@ -182,14 +182,18 @@ func (s *System) Serve(response http.ResponseWriter, fileKey string, name string
}
defer r.Close()
// All HTTP date/time stamps MUST be represented in Greenwich Mean Time (GMT)
// (see https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1)
location, _ := time.LoadLocation("GMT")
response.Header().Set("Content-Disposition", "attachment; filename="+name)
response.Header().Set("Content-Type", r.ContentType())
response.Header().Set("Content-Length", strconv.FormatInt(r.Size(), 10))
response.Header().Set("Last-Modified", r.ModTime().In(location).Format("Mon, 02 Jan 06 15:04:05 MST"))
// All HTTP date/time stamps MUST be represented in Greenwich Mean Time (GMT)
// (see https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1)
//
// NB! time.LoadLocation may fail on non-Unix systems (see https://github.com/pocketbase/pocketbase/issues/45)
location, locationErr := time.LoadLocation("GMT")
if locationErr == nil {
response.Header().Set("Last-Modified", r.ModTime().In(location).Format("Mon, 02 Jan 06 15:04:05 MST"))
}
// copy from the read range to response.
_, err := io.Copy(response, r)
+5 -2
View File
@@ -44,8 +44,11 @@ func (h *Hook[T]) Reset() {
// - any non-nil error is returned in one of the handlers
func (h *Hook[T]) Trigger(data T, oneOffHandlers ...Handler[T]) error {
h.mux.Lock()
handlers := append(h.handlers, oneOffHandlers...)
h.mux.Unlock() // unlock is not deferred to avoid deadlocks when Trigger is called recursive in the handlers
handlers := make([]Handler[T], 0, len(h.handlers)+len(oneOffHandlers))
handlers = append(handlers, h.handlers...)
handlers = append(handlers, oneOffHandlers...)
// unlock is not deferred to avoid deadlocks when Trigger is called recursive by the handlers
h.mux.Unlock()
for _, fn := range handlers {
err := fn(data)
+4 -37
View File
@@ -8,7 +8,6 @@ import (
var columnifyRemoveRegex = regexp.MustCompile(`[^\w\.\*\-\_\@\#]+`)
var snakecaseSplitRegex = regexp.MustCompile(`[\W_]+`)
var usernamifySplitRegex = regexp.MustCompile(`\W+`)
// UcFirst converts the first character of a string into uppercase.
func UcFirst(str string) string {
@@ -33,15 +32,14 @@ func Sentenize(str string) string {
return ""
}
s := []rune(str)
sentence := string(unicode.ToUpper(s[0])) + string(s[1:])
str = UcFirst(str)
lastChar := string(s[len(s)-1:])
lastChar := str[len(str)-1:]
if lastChar != "." && lastChar != "?" && lastChar != "!" {
return sentence + "."
return str + "."
}
return sentence
return str
}
// Sanitize sanitizes `str` by removing all characters satisfying `removePattern`.
@@ -85,34 +83,3 @@ func Snakecase(str string) string {
return strings.ToLower(result.String())
}
// Usernamify generates a properly formatted username from the provided string.
// Returns "unknown" if `str` is empty or contains only non word characters.
//
// ```go
// Usernamify("John Doe, hello") // "john.doe.hello"
// ```
func Usernamify(str string) string {
// split at any non word character
words := usernamifySplitRegex.Split(strings.ToLower(str), -1)
// concatenate any non empty word with a dot
var result strings.Builder
for _, word := range words {
if word == "" {
continue
}
if result.Len() > 0 {
result.WriteString(".")
}
result.WriteString(word)
}
if result.Len() == 0 {
return "unknown"
}
return result.String()
}
+5 -24
View File
@@ -12,6 +12,7 @@ func TestUcFirst(t *testing.T) {
expected string
}{
{"", ""},
{" ", " "},
{"Test", "Test"},
{"test", "Test"},
{"test test2", "Test test2"},
@@ -55,6 +56,9 @@ func TestSentenize(t *testing.T) {
}{
{"", ""},
{" ", ""},
{".", "."},
{"?", "?"},
{"!", "!"},
{"Test", "Test."},
{" test ", "Test."},
{"hello world", "Hello world."},
@@ -82,7 +86,7 @@ func TestSanitize(t *testing.T) {
{" ", ` `, "", false},
{"", `[A-Z]`, "", false},
{"abcABC", `[A-Z]`, "abc", false},
{"abcABC", `[A-Z`, "", true}, // invlid pattern
{"abcABC", `[A-Z`, "", true}, // invalid pattern
}
for i, scenario := range scenarios {
@@ -128,26 +132,3 @@ func TestSnakecase(t *testing.T) {
}
}
}
func TestUsernamify(t *testing.T) {
scenarios := []struct {
val string
expected string
}{
{"", "unknown"},
{" ", "unknown"},
{"!@#$%^", "unknown"},
{"...", "unknown"},
{"_", "_"}, // underscore is valid word character
{"John Doe", "john.doe"},
{"John_Doe", "john_doe"},
{".a!b@c#d$e%123. ", "a.b.c.d.e.123"},
{"Hello, world", "hello.world"},
}
for i, scenario := range scenarios {
if result := inflector.Usernamify(scenario.val); result != scenario.expected {
t.Errorf("(%d) Expected %q, got %q", i, scenario.expected, result)
}
}
}
+30 -29
View File
@@ -10,7 +10,7 @@ import (
var cachedPatterns = map[string]*regexp.Regexp{}
// ExustInSlice checks whether a comparable element exists in a slice of the same type.
// ExistInSlice checks whether a comparable element exists in a slice of the same type.
func ExistInSlice[T comparable](item T, list []T) bool {
if len(list) == 0 {
return false
@@ -38,21 +38,23 @@ func ExistInSliceWithRegex(str string, list []string) bool {
if str == field {
return true
}
} else {
// check for regex match
pattern, ok := cachedPatterns[field]
if !ok {
var patternErr error
pattern, patternErr = regexp.Compile(field)
if patternErr != nil {
continue
}
// "cache" the pattern to avoid compiling it every time
cachedPatterns[field] = pattern
}
if pattern != nil && pattern.MatchString(str) {
return true
continue
}
// check for regex match
pattern, ok := cachedPatterns[field]
if !ok {
var err error
pattern, err = regexp.Compile(field)
if err != nil {
continue
}
// "cache" the pattern to avoid compiling it every time
cachedPatterns[field] = pattern
}
if pattern != nil && pattern.MatchString(str) {
return true
}
}
@@ -72,46 +74,45 @@ func ToInterfaceSlice[T any](list []T) []any {
// NonzeroUniques returns only the nonzero unique values from a slice.
func NonzeroUniques[T comparable](list []T) []T {
result := []T{}
existMap := map[T]bool{}
result := make([]T, 0, len(list))
existMap := make(map[T]struct{}, len(list))
var zeroVal T
for _, val := range list {
if !existMap[val] && val != zeroVal {
existMap[val] = true
result = append(result, val)
if _, ok := existMap[val]; ok || val == zeroVal {
continue
}
existMap[val] = struct{}{}
result = append(result, val)
}
return result
}
// ToUniqueStringSlice casts `value` to a slice of non-zero unique strings.
func ToUniqueStringSlice(value any) []string {
strings := []string{}
func ToUniqueStringSlice(value any) (result []string) {
switch val := value.(type) {
case nil:
// nothing to cast
case []string:
strings = val
result = val
case string:
if val == "" {
break
}
// check if it is a json encoded array of strings
if err := json.Unmarshal([]byte(val), &strings); err != nil {
if err := json.Unmarshal([]byte(val), &result); err != nil {
// not a json array, just add the string as single array element
strings = append(strings, val)
result = append(result, val)
}
case json.Marshaler: // eg. JsonArray
raw, _ := val.MarshalJSON()
json.Unmarshal(raw, &strings)
_ = json.Unmarshal(raw, &result)
default:
strings = cast.ToStringSlice(value)
result = cast.ToStringSlice(value)
}
return NonzeroUniques(strings)
return NonzeroUniques(result)
}
-2
View File
@@ -37,8 +37,6 @@ func NewSmtpClient(
// SmtpClient defines a SMTP mail client structure that implements
// `mailer.Mailer` interface.
type SmtpClient struct {
mail *mailyak.MailYak
host string
port int
username string
+27 -52
View File
@@ -27,21 +27,16 @@ var parsedFilterData = store.New(make(map[string][]fexpr.ExprGroup, 50))
// BuildExpr parses the current filter data and returns a new db WHERE expression.
func (f FilterData) BuildExpr(fieldResolver FieldResolver) (dbx.Expression, error) {
raw := string(f)
var data []fexpr.ExprGroup
if parsedFilterData.Has(raw) {
data = parsedFilterData.Get(raw)
} else {
var err error
data, err = fexpr.Parse(raw)
if err != nil {
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 f.build(parsedFilterData.Get(raw), fieldResolver)
}
data, err := fexpr.Parse(raw)
if err != nil {
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 f.build(data, fieldResolver)
}
@@ -94,30 +89,18 @@ func (f FilterData) resolveTokenizedExpr(expr fexpr.Expr, fieldResolver FieldRes
// merge both operands parameters (if any)
params := dbx.Params{}
if len(lParams) > 0 {
for k, v := range lParams {
params[k] = v
}
for k, v := range lParams {
params[k] = v
}
if len(rParams) > 0 {
for k, v := range rParams {
params[k] = v
}
for k, v := range rParams {
params[k] = v
}
switch expr.Op {
case fexpr.SignEq:
op := "="
if strings.ToLower(lName) == "null" || strings.ToLower(rName) == "null" {
op = "IS"
}
return dbx.NewExp(fmt.Sprintf("%s %s %s", lName, op, rName), params), nil
return dbx.NewExp(fmt.Sprintf("COALESCE(%s, '') = COALESCE(%s, '')", lName, rName), params), nil
case fexpr.SignNeq:
op := "!="
if strings.ToLower(lName) == "null" || strings.ToLower(rName) == "null" {
op = "IS NOT"
}
return dbx.NewExp(fmt.Sprintf("%s %s %s", lName, op, rName), params), nil
return dbx.NewExp(fmt.Sprintf("COALESCE(%s, '') != COALESCE(%s, '')", lName, rName), params), nil
case fexpr.SignLike:
// normalize operands and switch sides if the left operand is a number or text
if len(lParams) > 0 {
@@ -144,35 +127,31 @@ func (f FilterData) resolveTokenizedExpr(expr fexpr.Expr, fieldResolver FieldRes
}
func (f FilterData) resolveToken(token fexpr.Token, fieldResolver FieldResolver) (name string, params dbx.Params, err error) {
if token.Type == fexpr.TokenIdentifier {
switch token.Type {
case fexpr.TokenIdentifier:
name, params, err := fieldResolver.Resolve(token.Literal)
if name == "" || err != nil {
// if `null` field is missing, treat `null` identifier as NULL token
if strings.ToLower(token.Literal) == "null" {
return "NULL", nil, nil
m := map[string]string{
// if `null` field is missing, treat `null` identifier as NULL token
"null": "NULL",
// if `true` field is missing, treat `true` identifier as TRUE token
"true": "1",
// if `false` field is missing, treat `false` identifier as FALSE token
"false": "0",
}
// if `true` field is missing, treat `true` identifier as TRUE token
if strings.ToLower(token.Literal) == "true" {
return "1", nil, nil
if v, ok := m[strings.ToLower(token.Literal)]; ok {
return v, nil, nil
}
// if `false` field is missing, treat `false` identifier as FALSE token
if strings.ToLower(token.Literal) == "false" {
return "0", nil, nil
}
return "", nil, err
}
return name, params, err
}
if token.Type == fexpr.TokenNumber || token.Type == fexpr.TokenText {
case fexpr.TokenNumber, fexpr.TokenText:
placeholder := "t" + security.RandomString(7)
name := fmt.Sprintf("{:%s}", placeholder)
params := dbx.Params{placeholder: token.Literal}
return name, params, nil
}
@@ -182,10 +161,6 @@ func (f FilterData) resolveToken(token fexpr.Token, fieldResolver FieldResolver)
func (f FilterData) normalizeLikeParams(params dbx.Params) dbx.Params {
result := dbx.Params{}
if len(params) == 0 {
return result
}
for k, v := range params {
vStr := cast.ToString(v)
if !strings.Contains(vStr, "%") {
+5 -5
View File
@@ -39,25 +39,25 @@ func TestFilterDataBuildExpr(t *testing.T) {
"^" +
regexp.QuoteMeta("((([[test1]] > {:") +
".+" +
regexp.QuoteMeta("}) OR ([[test2]] != {:") +
regexp.QuoteMeta("}) OR (COALESCE([[test2]], '') != COALESCE({:") +
".+" +
regexp.QuoteMeta("})) AND ([[test3]] LIKE {:") +
regexp.QuoteMeta("}, ''))) AND ([[test3]] LIKE {:") +
".+" +
regexp.QuoteMeta("})) AND ([[test4.sub]] IS NULL)") +
regexp.QuoteMeta("})) AND (COALESCE([[test4.sub]], '') = COALESCE(NULL, ''))") +
"$",
},
// combination of special literals (null, true, false)
{
"test1=true && test2 != false && test3 = null || test4.sub != null",
false,
"^" + regexp.QuoteMeta("((([[test1]] = 1) AND ([[test2]] != 0)) AND ([[test3]] IS NULL)) OR ([[test4.sub]] IS NOT NULL)") + "$",
"^" + regexp.QuoteMeta("(((COALESCE([[test1]], '') = COALESCE(1, '')) AND (COALESCE([[test2]], '') != COALESCE(0, ''))) AND (COALESCE([[test3]], '') = COALESCE(NULL, ''))) OR (COALESCE([[test4.sub]], '') != COALESCE(NULL, ''))") + "$",
},
// all operators
{
"(test1 = test2 || test2 != test3) && (test2 ~ 'example' || test2 !~ '%%abc') && 'switch1%%' ~ test1 && 'switch2' !~ test2 && test3 > 1 && test3 >= 0 && test3 <= 4 && 2 < 5",
false,
"^" +
regexp.QuoteMeta("(((((((([[test1]] = [[test2]]) OR ([[test2]] != [[test3]])) AND (([[test2]] LIKE {:") +
regexp.QuoteMeta("((((((((COALESCE([[test1]], '') = COALESCE([[test2]], '')) OR (COALESCE([[test2]], '') != COALESCE([[test3]], ''))) AND (([[test2]] LIKE {:") +
".+" +
regexp.QuoteMeta("}) OR ([[test2]] NOT LIKE {:") +
".+" +
+21 -30
View File
@@ -121,8 +121,7 @@ func (s *Provider) Parse(urlQuery string) error {
return err
}
rawPage := params.Get(PageQueryParam)
if rawPage != "" {
if rawPage := params.Get(PageQueryParam); rawPage != "" {
page, err := strconv.Atoi(rawPage)
if err != nil {
return err
@@ -130,8 +129,7 @@ func (s *Provider) Parse(urlQuery string) error {
s.Page(page)
}
rawPerPage := params.Get(PerPageQueryParam)
if rawPerPage != "" {
if rawPerPage := params.Get(PerPageQueryParam); rawPerPage != "" {
perPage, err := strconv.Atoi(rawPerPage)
if err != nil {
return err
@@ -139,15 +137,13 @@ func (s *Provider) Parse(urlQuery string) error {
s.PerPage(perPage)
}
rawSort := params.Get(SortQueryParam)
if rawSort != "" {
if rawSort := params.Get(SortQueryParam); rawSort != "" {
for _, sortField := range ParseSortFromString(rawSort) {
s.AddSort(sortField)
}
}
rawFilter := params.Get(FilterQueryParam)
if rawFilter != "" {
if rawFilter := params.Get(FilterQueryParam); rawFilter != "" {
s.AddFilter(FilterData(rawFilter))
}
@@ -165,35 +161,30 @@ func (s *Provider) Exec(items any) (*Result, error) {
modelsQuery := *s.query
// apply filters
if len(s.filter) > 0 {
for _, f := range s.filter {
expr, err := f.BuildExpr(s.fieldResolver)
if err != nil {
return nil, err
}
if expr != nil {
modelsQuery.AndWhere(expr)
}
for _, f := range s.filter {
expr, err := f.BuildExpr(s.fieldResolver)
if err != nil {
return nil, err
}
if expr != nil {
modelsQuery.AndWhere(expr)
}
}
// apply sorting
if len(s.sort) > 0 {
for _, sortField := range s.sort {
expr, err := sortField.BuildExpr(s.fieldResolver)
if err != nil {
return nil, err
}
if expr != "" {
modelsQuery.AndOrderBy(expr)
}
for _, sortField := range s.sort {
expr, err := sortField.BuildExpr(s.fieldResolver)
if err != nil {
return nil, err
}
if expr != "" {
modelsQuery.AndOrderBy(expr)
}
}
// apply field resolver query modifications (if any)
updateQueryErr := s.fieldResolver.UpdateQuery(&modelsQuery)
if updateQueryErr != nil {
return nil, updateQueryErr
if err := s.fieldResolver.UpdateQuery(&modelsQuery); err != nil {
return nil, err
}
// count
@@ -210,7 +201,7 @@ func (s *Provider) Exec(items any) (*Result, error) {
s.perPage = MaxPerPage
}
// normalize page accoring to the total count
// normalize page according to the total count
if s.page <= 0 || totalCount == 0 {
s.page = 1
} else if totalPages := int(math.Ceil(float64(totalCount) / float64(s.perPage))); s.page > totalPages {
+5 -5
View File
@@ -273,8 +273,8 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
false,
`{"page":1,"perPage":` + fmt.Sprint(MaxPerPage) + `,"totalItems":1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
[]string{
"SELECT count(*) FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (test2 IS NOT null)) AND (test1 >= '2') ORDER BY `test1` ASC, `test2` DESC",
"SELECT * FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (test2 IS NOT null)) AND (test1 >= '2') ORDER BY `test1` ASC, `test2` DESC LIMIT 200",
"SELECT count(*) FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= '2') ORDER BY `test1` ASC, `test2` DESC",
"SELECT * FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= '2') ORDER BY `test1` ASC, `test2` DESC LIMIT 200",
},
},
// valid sort and filter fields (zero results)
@@ -286,8 +286,8 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
false,
`{"page":1,"perPage":10,"totalItems":0,"items":[]}`,
[]string{
"SELECT count(*) FROM `test` WHERE (NOT (`test1` IS NULL)) AND (test3 != '') ORDER BY `test1` ASC, `test3` ASC",
"SELECT * FROM `test` WHERE (NOT (`test1` IS NULL)) AND (test3 != '') ORDER BY `test1` ASC, `test3` ASC LIMIT 10",
"SELECT count(*) FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', '')) ORDER BY `test1` ASC, `test3` ASC",
"SELECT * FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', '')) ORDER BY `test1` ASC, `test3` ASC LIMIT 10",
},
},
// pagination test
@@ -344,7 +344,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
for _, q := range testDB.CalledQueries {
if !list.ExistInSliceWithRegex(q, s.expectQueries) {
t.Errorf("(%d) Didn't expect query %v", i, q)
t.Errorf("(%d) Didn't expect query \n%v", i, q)
}
}
}
+4 -12
View File
@@ -34,26 +34,18 @@ func (s *SortField) BuildExpr(fieldResolver FieldResolver) (string, error) {
//
// Example:
// fields := search.ParseSortFromString("-name,+created")
func ParseSortFromString(str string) []SortField {
result := []SortField{}
func ParseSortFromString(str string) (fields []SortField) {
data := strings.Split(str, ",")
for _, field := range data {
// trim whitespaces
field = strings.TrimSpace(field)
var dir string
if strings.HasPrefix(field, "-") {
dir = SortDesc
field = strings.TrimPrefix(field, "-")
fields = append(fields, SortField{strings.TrimPrefix(field, "-"), SortDesc})
} else {
dir = SortAsc
field = strings.TrimPrefix(field, "+")
fields = append(fields, SortField{strings.TrimPrefix(field, "+"), SortAsc})
}
result = append(result, SortField{field, dir})
}
return result
return
}
+1 -6
View File
@@ -66,10 +66,5 @@ func Decrypt(cipherText string, key string) ([]byte, error) {
}
nonce, cipherByteClean := cipherByte[:nonceSize], cipherByte[nonceSize:]
plainData, err := gcm.Open(nil, nonce, cipherByteClean, nil)
if err != nil {
return nil, err
}
return plainData, nil
return gcm.Open(nil, nonce, cipherByteClean, nil)
}
+3 -7
View File
@@ -24,9 +24,7 @@ func ParseUnverifiedJWT(token string) (jwt.MapClaims, error) {
// ParseJWT verifies and parses JWT token and returns its claims.
func ParseJWT(token string, verificationKey string) (jwt.MapClaims, error) {
parser := &jwt.Parser{
ValidMethods: []string{"HS256"},
}
parser := jwt.NewParser(jwt.WithValidMethods([]string{"HS256"}))
parsedToken, err := parser.Parse(token, func(t *jwt.Token) (any, error) {
return []byte(verificationKey), nil
@@ -50,10 +48,8 @@ func NewToken(payload jwt.MapClaims, signingKey string, secondsDuration int64) (
"exp": time.Now().Add(seconds).Unix(),
}
if len(payload) > 0 {
for k, v := range payload {
claims[k] = v
}
for k, v := range payload {
claims[k] = v
}
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(signingKey))
+2 -1
View File
@@ -9,6 +9,7 @@ import (
func TestRandomString(t *testing.T) {
generated := []string{}
reg := regexp.MustCompile(`[a-zA-Z0-9]+`)
for i := 0; i < 30; i++ {
length := 5 + i
@@ -18,7 +19,7 @@ func TestRandomString(t *testing.T) {
t.Errorf("(%d) Expected the length of the string to be %d, got %d", i, length, len(result))
}
if match, _ := regexp.MatchString("[a-zA-Z0-9]+", result); !match {
if match := reg.MatchString(result); !match {
t.Errorf("(%d) The generated strings should have only [a-zA-Z0-9]+ characters, got %q", i, result)
}
+8
View File
@@ -13,6 +13,14 @@ func New[T any](data map[string]T) *Store[T] {
return &Store[T]{data: 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)
}
// Remove removes a single entry from the store.
//
// Remove does nothing if key doesn't exist in the store.
+14
View File
@@ -14,6 +14,20 @@ func TestNew(t *testing.T) {
}
}
func TestRemoveAll(t *testing.T) {
s := store.New(map[string]bool{"test1": true, "test2": true})
keys := []string{"test1", "test2"}
s.RemoveAll()
for i, key := range keys {
if s.Has(key) {
t.Errorf("(%d) Expected %q to be removed", i, key)
}
}
}
func TestRemove(t *testing.T) {
s := store.New(map[string]bool{"test": true})
+2 -1
View File
@@ -1,10 +1,11 @@
package types_test
import (
"github.com/pocketbase/pocketbase/tools/types"
"strings"
"testing"
"time"
"github.com/pocketbase/pocketbase/tools/types"
)
func TestNowDateTime(t *testing.T) {
+1
View File
@@ -2,3 +2,4 @@
PB_BACKEND_URL = /
PB_PROFILE_COLLECTION = profiles
PB_RULES_SYNTAX_DOCS = https://pocketbase.io/docs/manage-collections#rules-filters-syntax
PB_INSTALLER_PARAM = installer
-68
View File
@@ -1,68 +0,0 @@
import{S as ln,i as tn,s as sn,O as We,a as en,b as nn,c as fn,e,d as n,f as Xt,g as f,h as i,j as u,m as Dt,k as bn,t as un,l as on,n as zt,o as Ft,p as s,q as Gt,r as pn,u as Ye}from"./index.66a4e29c.js";function mn(v){let b;return{c(){b=e("p"),b.textContent="Lorem ipsum dolor sit amet..."},m(o,p){i(o,b,p)},p:Ye,d(o){o&&s(b)}}}function vn(v){let b;return{c(){b=e("h4"),b.textContent="My title",f(b,"slot","header")},m(o,p){i(o,b,p)},p:Ye,d(o){o&&s(b)}}}function dn(v){let b,o,p;return{c(){b=e("button"),b.textContent="Cancel",o=n(),p=e("button"),p.textContent="Save",f(b,"class","btn btn-secondary"),f(p,"class","btn btn-expanded")},m(m,d){i(m,b,d),i(m,o,d),i(m,p,d)},p:Ye,d(m){m&&s(b),m&&s(o),m&&s(p)}}}function xn(v){let b,o,p,m,d,Jt,Kt,_,_t,Ze,y,Qt,Ut,Vt,L,Lt,re,B,Wt,Yt,Zt,r,rt,c,ct,a,at,h,ht,g,gt,li,ti,Ht,ii,Ct,si,Mt,ei,Tt,ni,yt,fi,Bt,bi,wt,ui,kt,oi,$t,pi,Ot,mi,It,vi,Et,di,xi,_i,ll,Li,Nt,Hi,Pt,Ci,jt,Mi,At,Ti,yi,Bi,tl,wi,ki,$i,il,Oi,sl,Ii,el,Ei,nl,Ni,fl,Pi,bl,ji,Ai,Si,ul,qi,ol,Ri,pl,Xi,ml,Di,vl,zi,dl,Fi,Gi,Ji,xl,Ki,_l,Qi,Ll,Ui,Hl,Vi,Cl,Wi,Ml,Yi,Zi,ri,w,ci,k,ai,$,hi,O,gi,I,ls,E,ts,N,is,P,ss,j,es,A,ns,S,fs,q,bs,us,os,Tl,ps,yl,ms,Bl,vs,wl,ds,kl,xs,_s,Ls,$l,Hs,Ol,Cs,Il,Ms,El,Ts,Nl,ys,Bs,ws,Pl,ks,jl,$s,Al,Os,Sl,Is,ql,Es,Ns,Ps,Rl,js,Xl,As,Dl,Ss,zl,qs,Fl,Rs,Gl,Xs,Ds,zs,Jl,Fs,Kl,Gs,Ql,Js,Ul,Ks,Vl,Qs,Wl,Us,Vs,Ws,R,Ys,X,Zs,D,rs,z,cs,F,as,G,hs,gs,le,St,te,ie,se,J,H,K,Q,ee,ne,fe,Yl,be,ue,oe,Zl,pe,rl,me,C,qt,ce,U,M,V,W,ve,de,xe,cl,_e,al,Le,hl,gl,T,Y,Z,He,Ce,Me,lt,Te,tt,ye,it,Be,st,we,ke,$e,et,Oe,nt,Ie,ft,Ee,bt,Ne,Pe,je,ut,Ae,ot,Se,pt,qe,mt,Re,Xe,De,vt,ze,dt,Fe,Ge,Je,xt,Ke,x,Qe,Rt,Ue,ae;m=new We({props:{multiple:!0,searchable:!0,items:["test1","test2"]}}),y=new We({props:{searchable:!0,items:["test1","test2"]}}),B=new We({props:{disabled:!0,searchable:!0,items:["test1","test2"]}});function ge(l){v[1](l)}let he={popup:!1,$$slots:{footer:[dn],header:[vn],default:[mn]},$$scope:{ctx:v}};return v[0]!==void 0&&(he.active=v[0]),x=new en({props:he}),nn.push(()=>fn(x,"active",ge)),{c(){b=e("div"),o=e("label"),o.textContent="EXAMPLE",p=n(),Xt(m.$$.fragment),d=n(),Jt=e("hr"),Kt=n(),_=e("div"),_t=e("label"),_t.textContent="EXAMPLE",Ze=n(),Xt(y.$$.fragment),Qt=n(),Ut=e("hr"),Vt=n(),L=e("div"),Lt=e("label"),Lt.textContent="EXAMPLE",re=n(),Xt(B.$$.fragment),Wt=n(),Yt=e("hr"),Zt=n(),r=e("div"),r.innerHTML=`<div class="icon"><i class="ri-information-line"></i></div>
<div class="content">Hello world!</div>
<div class="close"><i class="ri-close-line"></i></div>`,rt=n(),c=e("div"),c.innerHTML=`<div class="icon"><i class="ri-information-line"></i></div>
<div class="content">Hello world!</div>
<div class="close"><i class="ri-close-line"></i></div>`,ct=n(),a=e("div"),a.innerHTML=`<div class="icon"><i class="ri-information-line"></i></div>
<div class="content">Hello world!</div>
<div class="close"><i class="ri-close-line"></i></div>`,at=n(),h=e("div"),h.innerHTML=`<div class="icon"><i class="ri-error-warning-line"></i></div>
<div class="content">Hello world!</div>
<div class="close"><i class="ri-close-line"></i></div>`,ht=n(),g=e("div"),g.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
<div class="content">Hello world!</div>
<div class="close"><i class="ri-close-line"></i></div>`,gt=n(),li=e("hr"),ti=n(),Ht=e("h1"),Ht.textContent="H1 title",ii=n(),Ct=e("p"),Ct.textContent="Lorem Ipsum dolor sit amet...",si=n(),Mt=e("h2"),Mt.textContent="H2 title",ei=n(),Tt=e("p"),Tt.textContent="Lorem Ipsum dolor sit amet...",ni=n(),yt=e("h3"),yt.textContent="H3 title",fi=n(),Bt=e("p"),Bt.textContent="Lorem Ipsum dolor sit amet...",bi=n(),wt=e("h4"),wt.textContent="H4 title",ui=n(),kt=e("p"),kt.textContent="Lorem Ipsum dolor sit amet...",oi=n(),$t=e("h5"),$t.textContent="H5 title",pi=n(),Ot=e("p"),Ot.textContent="Lorem Ipsum dolor sit amet...",mi=n(),It=e("h6"),It.textContent="H6 title",vi=n(),Et=e("p"),Et.textContent="Lorem Ipsum dolor sit amet...",di=n(),xi=e("hr"),_i=n(),ll=e("div"),ll.innerHTML=`<div class="col-6">COL1</div>
<div class="col-6">COL2</div>`,Li=n(),Nt=e("p"),Nt.innerHTML=`Lorem Ipsum is <a href="/">simply dummy</a> text of the printing and typesetting industry. Lorem Ipsum has
been the industry&#39;s
<strong>standard</strong> dummy text ever since the 1500s, when an unknown printer took a galley of type
and <em>scrambled</em> it to make a type specimen book. It has survived not only five centuries, but also
the leap into electronic typesetting, remaining<sup>1</sup> essentially<sub>2</sub> unchanged.`,Hi=n(),Pt=e("p"),Pt.textContent=`It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and
more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum`,Ci=n(),jt=e("ul"),jt.innerHTML=`<li><small>Option 1</small></li>
<li>Option 2</li>
<li>Option 3</li>`,Mi=n(),At=e("ol"),At.innerHTML=`<li>Option 1</li>
<li>Option 2</li>
<li>Option 3</li>`,Ti=n(),yi=e("hr"),Bi=n(),tl=e("span"),tl.textContent="Lorem Ipsum",wi=n(),ki=e("hr"),$i=n(),il=e("button"),il.textContent="Button default",Oi=n(),sl=e("button"),sl.textContent="Button danger",Ii=n(),el=e("button"),el.textContent="Button warning",Ei=n(),nl=e("button"),nl.textContent="Button success",Ni=n(),fl=e("button"),fl.textContent="Button info",Pi=n(),bl=e("button"),bl.textContent="Button hint",ji=n(),Ai=e("hr"),Si=n(),ul=e("button"),ul.textContent="Button default",qi=n(),ol=e("button"),ol.textContent="Button danger",Ri=n(),pl=e("button"),pl.textContent="Button danger",Xi=n(),ml=e("button"),ml.textContent="Button success",Di=n(),vl=e("button"),vl.textContent="Button info",zi=n(),dl=e("button"),dl.textContent="Button hint",Fi=n(),Gi=e("hr"),Ji=n(),xl=e("button"),xl.textContent="Button default",Ki=n(),_l=e("button"),_l.textContent="Button danger",Qi=n(),Ll=e("button"),Ll.textContent="Button danger",Ui=n(),Hl=e("button"),Hl.textContent="Button success",Vi=n(),Cl=e("button"),Cl.textContent="Button info",Wi=n(),Ml=e("button"),Ml.textContent="Button hint",Yi=n(),Zi=e("hr"),ri=n(),w=e("button"),w.textContent="Button default",ci=n(),k=e("button"),k.textContent="Button danger",ai=n(),$=e("button"),$.textContent="Button warning",hi=n(),O=e("button"),O.textContent="Button success",gi=n(),I=e("button"),I.textContent="Button info",ls=n(),E=e("button"),E.textContent="Button hint",ts=n(),N=e("button"),N.textContent="Button default",is=n(),P=e("button"),P.textContent="Button danger",ss=n(),j=e("button"),j.textContent="Button danger",es=n(),A=e("button"),A.textContent="Button success",ns=n(),S=e("button"),S.textContent="Button info",fs=n(),q=e("button"),q.textContent="Button hint",bs=n(),us=e("hr"),os=n(),Tl=e("button"),Tl.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button default</span>`,ps=n(),yl=e("button"),yl.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button danger</span>`,ms=n(),Bl=e("button"),Bl.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button warning</span>`,vs=n(),wl=e("button"),wl.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button success</span>`,ds=n(),kl=e("button"),kl.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button hint</span>`,xs=n(),_s=e("hr"),Ls=n(),$l=e("button"),$l.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button default</span>`,Hs=n(),Ol=e("button"),Ol.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button danger</span>`,Cs=n(),Il=e("button"),Il.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button warning</span>`,Ms=n(),El=e("button"),El.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button success</span>`,Ts=n(),Nl=e("button"),Nl.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button hint</span>`,ys=n(),Bs=e("hr"),ws=n(),Pl=e("button"),Pl.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button default</span>`,ks=n(),jl=e("button"),jl.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button danger</span>`,$s=n(),Al=e("button"),Al.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button warning</span>`,Os=n(),Sl=e("button"),Sl.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button success</span>`,Is=n(),ql=e("button"),ql.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button hint</span>`,Es=n(),Ns=e("hr"),Ps=n(),Rl=e("button"),Rl.innerHTML='<i class="ri-mail-line"></i>',js=n(),Xl=e("button"),Xl.innerHTML='<i class="ri-mail-line"></i>',As=n(),Dl=e("button"),Dl.innerHTML='<i class="ri-mail-line"></i>',Ss=n(),zl=e("button"),zl.innerHTML='<i class="ri-mail-line"></i>',qs=n(),Fl=e("button"),Fl.innerHTML='<i class="ri-mail-line"></i>',Rs=n(),Gl=e("button"),Gl.innerHTML='<i class="ri-mail-line"></i>',Xs=n(),Ds=e("hr"),zs=n(),Jl=e("button"),Jl.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button Loading</span>`,Fs=n(),Kl=e("button"),Kl.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button Loading</span>`,Gs=n(),Ql=e("button"),Ql.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button Loading</span>`,Js=n(),Ul=e("button"),Ul.innerHTML='<i class="ri-mail-line"></i>',Ks=n(),Vl=e("button"),Vl.innerHTML='<i class="ri-mail-line"></i>',Qs=n(),Wl=e("button"),Wl.innerHTML='<i class="ri-mail-line"></i>',Us=n(),Vs=e("hr"),Ws=n(),R=e("button"),R.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button Loading</span>`,Ys=n(),X=e("button"),X.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button Loading</span>`,Zs=n(),D=e("button"),D.innerHTML=`<i class="ri-mail-line"></i>
<span class="txt">Button Loading</span>`,rs=n(),z=e("button"),z.innerHTML='<i class="ri-mail-line"></i>',cs=n(),F=e("button"),F.innerHTML='<i class="ri-mail-line"></i>',as=n(),G=e("button"),G.innerHTML='<i class="ri-mail-line"></i>',hs=n(),gs=e("hr"),le=n(),St=e("input"),te=n(),ie=e("hr"),se=n(),J=e("select"),H=e("option"),H.textContent="Option 1",K=e("option"),K.textContent="Option 2",Q=e("option"),Q.textContent="Option 3",ee=n(),ne=e("hr"),fe=n(),Yl=e("textarea"),be=n(),ue=e("hr"),oe=n(),Zl=e("div"),Zl.innerHTML=`<label for="field_1">Name</label>
<input type="text" id="field_1" placeholder="Name 123"/>`,pe=n(),rl=e("div"),rl.innerHTML=`<label for="field_2">Description</label>
<textarea id="field_2"></textarea>`,me=n(),C=e("div"),qt=e("label"),qt.textContent="Choose value",ce=n(),U=e("select"),M=e("option"),M.textContent="Option 1",V=e("option"),V.textContent="Option 2",W=e("option"),W.textContent="Option 3",ve=n(),de=e("hr"),xe=n(),cl=e("div"),cl.innerHTML='<input type="text" placeholder="Lorem ipsum dolor sit amet..."/>',_e=n(),al=e("div"),al.innerHTML="<textarea></textarea>",Le=n(),hl=e("div"),gl=e("select"),T=e("option"),T.textContent="Option 1",Y=e("option"),Y.textContent="Option 2",Z=e("option"),Z.textContent="Option 3",He=n(),Ce=e("hr"),Me=n(),lt=e("div"),lt.innerHTML=`<input type="checkbox" id="field_check"/>
<label for="field_check">I agree with the <a href="/">terms and conditions</a></label>`,Te=n(),tt=e("div"),tt.innerHTML=`<input type="radio" name="radio_check" id="field_radio1" value="1"/>
<label for="field_radio1">Radio 1</label>`,ye=n(),it=e("div"),it.innerHTML=`<input type="radio" name="radio_check" id="field_radio2" value="2"/>
<label for="field_radio2">Radio 2</label>`,Be=n(),st=e("div"),st.innerHTML=`<input type="checkbox" id="field_toggle"/>
<label for="field_toggle">Toggle check</label>`,we=n(),ke=e("hr"),$e=n(),et=e("div"),et.innerHTML=`<label for="field_error">Name + error</label>
<input type="text" id="field_error"/>
<div class="help-block help-block-error"><p>Something went wrong</p></div>`,Oe=n(),nt=e("div"),nt.innerHTML=`<label for="field_hint">Name + hint</label>
<input type="text" id="field_hint"/>
<div class="help-block"><p>Lorem ipsum dolor <a href="/">sit</a> amet</p></div>`,Ie=n(),ft=e("div"),ft.innerHTML=`<input type="checkbox" id="field_check_hint"/>
<label for="field_check_hint">Checkbox hint</label>
<div class="help-block">Lorem ipsum</div>`,Ee=n(),bt=e("div"),bt.innerHTML=`<input type="checkbox" id="field_check_error"/>
<label for="field_check_error">Checkbox error</label>
<div class="help-block help-error">Lorem ipsum</div>`,Ne=n(),Pe=e("hr"),je=n(),ut=e("div"),ut.innerHTML=`<input type="checkbox" id="field_check" disabled=""/>
<label for="field_check">I agree with the <a href="/">terms and conditions</a></label>`,Ae=n(),ot=e("div"),ot.innerHTML=`<input type="radio" name="radio_check" id="field_radio1" value="1" disabled=""/>
<label for="field_radio1">Radio 1</label>`,Se=n(),pt=e("div"),pt.innerHTML=`<input type="radio" name="radio_check" id="field_radio2" value="2" disabled=""/>
<label for="field_radio2">Radio 2</label>`,qe=n(),mt=e("div"),mt.innerHTML=`<input type="checkbox" id="field_toggle" disabled=""/>
<label for="field_toggle"></label>`,Re=n(),Xe=e("hr"),De=n(),vt=e("div"),vt.innerHTML=`<label for="field_addon1">Name</label>
<div class="form-field-addon"><i class="ri-mail-line"></i></div>
<input disabled="" type="text" id="field_addon1"/>`,ze=n(),dt=e("div"),dt.innerHTML=`<div class="form-field-addon"><i class="ri-mail-line"></i></div>
<input type="text"/>`,Fe=n(),Ge=e("hr"),Je=n(),xt=e("div"),xt.innerHTML=`<div class="form-field"><label for="field_group1">Name</label>
<div class="form-field-addon"><div class="btn btn-circle btn-secondary"><i class="ri-mail-line"></i></div></div>
<input type="text" id="field_group1"/></div>
<div class="form-field"><label for="field_group2">Password</label>
<div class="form-field-addon"><i class="ri-mail-line"></i></div>
<input type="password" id="field_group2"/></div>`,Ke=n(),Xt(x.$$.fragment),f(o,"for",""),f(b,"class","form-field"),f(_t,"for",""),f(_,"class","form-field"),f(Lt,"for",""),f(L,"class","form-field disabled"),f(r,"class","alert"),f(c,"class","alert alert-info"),f(a,"class","alert alert-danger"),f(h,"class","alert alert-warning"),f(g,"class","alert alert-success"),f(ll,"class","grid"),f(il,"class","btn"),f(sl,"class","btn btn-danger"),f(el,"class","btn btn-warning"),f(nl,"class","btn btn-success"),f(fl,"class","btn btn-info"),f(bl,"class","btn btn-hint"),f(ul,"class","btn btn-secondary"),f(ol,"class","btn btn-secondary btn-danger"),f(pl,"class","btn btn-secondary btn-warning"),f(ml,"class","btn btn-secondary btn-success"),f(vl,"class","btn btn-secondary btn-info"),f(dl,"class","btn btn-secondary btn-hint"),f(xl,"class","btn btn-outline"),f(_l,"class","btn btn-outline btn-danger"),f(Ll,"class","btn btn-outline btn-warning"),f(Hl,"class","btn btn-outline btn-success"),f(Cl,"class","btn btn-outline btn-info"),f(Ml,"class","btn btn-outline btn-hint"),w.disabled=!0,f(w,"class","btn"),k.disabled=!0,f(k,"class","btn btn-danger"),$.disabled=!0,f($,"class","btn btn-warning"),O.disabled=!0,f(O,"class","btn btn-success"),I.disabled=!0,f(I,"class","btn btn-info"),E.disabled=!0,f(E,"class","btn btn-hint"),N.disabled=!0,f(N,"class","btn btn-secondary"),P.disabled=!0,f(P,"class","btn btn-secondary btn-danger"),j.disabled=!0,f(j,"class","btn btn-secondary btn-warning"),A.disabled=!0,f(A,"class","btn btn-secondary btn-success"),S.disabled=!0,f(S,"class","btn btn-secondary btn-info"),q.disabled=!0,f(q,"class","btn btn-secondary btn-hint"),f(Tl,"class","btn"),f(yl,"class","btn btn-danger"),f(Bl,"class","btn btn-warning"),f(wl,"class","btn btn-success"),f(kl,"class","btn btn-hint"),f($l,"class","btn btn-sm"),f(Ol,"class","btn btn-danger btn-sm"),f(Il,"class","btn btn-warning btn-sm"),f(El,"class","btn btn-success btn-sm"),f(Nl,"class","btn btn-hint btn-sm"),f(Pl,"class","btn btn-lg"),f(jl,"class","btn btn-danger btn-lg"),f(Al,"class","btn btn-warning btn-lg"),f(Sl,"class","btn btn-success btn-lg"),f(ql,"class","btn btn-hint btn-lg"),f(Rl,"class","btn btn-circle"),f(Xl,"class","btn btn-sm btn-circle"),f(Dl,"class","btn btn-lg btn-circle"),f(zl,"class","btn btn-secondary btn-circle"),f(Fl,"class","btn btn-secondary btn-sm btn-circle"),f(Gl,"class","btn btn-secondary btn-lg btn-circle"),f(Jl,"class","btn btn-loading"),f(Kl,"class","btn btn-loading btn-primary btn-sm"),f(Ql,"class","btn btn-loading btn-danger btn-lg"),f(Ul,"class","btn btn-loading btn-circle"),f(Vl,"class","btn btn-loading btn-primary btn-sm btn-circle"),f(Wl,"class","btn btn-loading btn-danger btn-lg btn-circle"),R.disabled=!0,f(R,"class","btn btn-loading"),X.disabled=!0,f(X,"class","btn btn-loading btn-primary btn-sm"),D.disabled=!0,f(D,"class","btn btn-loading btn-danger btn-lg"),z.disabled=!0,f(z,"class","btn btn-loading btn-circle"),F.disabled=!0,f(F,"class","btn btn-loading btn-primary btn-sm btn-circle"),G.disabled=!0,f(G,"class","btn btn-loading btn-danger btn-lg btn-circle"),f(St,"type","text"),H.__value="1",H.value=H.__value,H.selected=!0,K.__value="",K.value=K.__value,Q.__value="",Q.value=Q.__value,f(Yl,"cols","30"),f(Yl,"rows","10"),f(Zl,"class","form-field required"),f(rl,"class","form-field required"),f(qt,"for","field_3"),M.__value="1",M.value=M.__value,M.selected=!0,V.__value="",V.value=V.__value,W.__value="",W.value=W.__value,f(U,"id","field_3"),f(C,"class","form-field"),f(cl,"class","form-field"),f(al,"class","form-field"),T.__value="1",T.value=T.__value,T.selected=!0,Y.__value="",Y.value=Y.__value,Z.__value="",Z.value=Z.__value,f(hl,"class","form-field"),f(lt,"class","form-field"),f(tt,"class","form-field"),f(it,"class","form-field"),f(st,"class","form-field form-field-toggle"),f(et,"class","form-field error"),f(nt,"class","form-field"),f(ft,"class","form-field"),f(bt,"class","form-field has-error"),f(ut,"class","form-field disabled"),f(ot,"class","form-field"),f(pt,"class","form-field"),f(mt,"class","form-field form-field-toggle disabled"),f(vt,"class","form-field disabled"),f(dt,"class","form-field"),f(xt,"class","form-group")},m(l,t){i(l,b,t),u(b,o),u(b,p),Dt(m,b,null),i(l,d,t),i(l,Jt,t),i(l,Kt,t),i(l,_,t),u(_,_t),u(_,Ze),Dt(y,_,null),i(l,Qt,t),i(l,Ut,t),i(l,Vt,t),i(l,L,t),u(L,Lt),u(L,re),Dt(B,L,null),i(l,Wt,t),i(l,Yt,t),i(l,Zt,t),i(l,r,t),i(l,rt,t),i(l,c,t),i(l,ct,t),i(l,a,t),i(l,at,t),i(l,h,t),i(l,ht,t),i(l,g,t),i(l,gt,t),i(l,li,t),i(l,ti,t),i(l,Ht,t),i(l,ii,t),i(l,Ct,t),i(l,si,t),i(l,Mt,t),i(l,ei,t),i(l,Tt,t),i(l,ni,t),i(l,yt,t),i(l,fi,t),i(l,Bt,t),i(l,bi,t),i(l,wt,t),i(l,ui,t),i(l,kt,t),i(l,oi,t),i(l,$t,t),i(l,pi,t),i(l,Ot,t),i(l,mi,t),i(l,It,t),i(l,vi,t),i(l,Et,t),i(l,di,t),i(l,xi,t),i(l,_i,t),i(l,ll,t),i(l,Li,t),i(l,Nt,t),i(l,Hi,t),i(l,Pt,t),i(l,Ci,t),i(l,jt,t),i(l,Mi,t),i(l,At,t),i(l,Ti,t),i(l,yi,t),i(l,Bi,t),i(l,tl,t),i(l,wi,t),i(l,ki,t),i(l,$i,t),i(l,il,t),i(l,Oi,t),i(l,sl,t),i(l,Ii,t),i(l,el,t),i(l,Ei,t),i(l,nl,t),i(l,Ni,t),i(lLine truncated
@@ -1,12 +1,12 @@
import{S as Sa,i as va,s as Ca,e as Aa,g as Ma,h as Da,u as xn,p as Oa,M as Ta,N as Ba,P as Ra,Q as Pa,R as La,H as kn,b as Ea}from"./index.66a4e29c.js";class z{constructor(){}lineAt(e){if(e<0||e>this.length)throw new RangeError(`Invalid position ${e} in document of length ${this.length}`);return this.lineInner(e,!1,1,0)}line(e){if(e<1||e>this.lines)throw new RangeError(`Invalid line number ${e} in ${this.lines}-line document`);return this.lineInner(e,!0,1,0)}replace(e,t,i){let s=[];return this.decompose(0,e,s,2),i.length&&i.decompose(0,i.length,s,3),this.decompose(t,this.length,s,1),Ve.from(s,this.length-(t-e)+i.length)}append(e){return this.replace(this.length,this.length,e)}slice(e,t=this.length){let i=[];return this.decompose(e,t,i,0),Ve.from(i,t-e)}eq(e){if(e==this)return!0;if(e.length!=this.length||e.lines!=this.lines)return!1;let t=this.scanIdentical(e,1),i=this.length-this.scanIdentical(e,-1),s=new Zt(this),r=new Zt(e);for(let o=t,l=t;;){if(s.next(o),r.next(o),o=0,s.lineBreak!=r.lineBreak||s.done!=r.done||s.value!=r.value)return!1;if(l+=s.value.length,s.done||l>=i)return!0}}iter(e=1){return new Zt(this,e)}iterRange(e,t=this.length){return new zo(this,e,t)}iterLines(e,t){let i;if(e==null)i=this.iter();else{t==null&&(t=this.lines+1);let s=this.line(e).from;i=this.iterRange(s,Math.max(s,t==this.lines+1?this.length:t<=1?0:this.line(t-1).to))}return new qo(i)}toString(){return this.sliceString(0)}toJSON(){let e=[];return this.flatten(e),e}static of(e){if(e.length==0)throw new RangeError("A document must have at least one line");return e.length==1&&!e[0]?z.empty:e.length<=32?new Q(e):Ve.from(Q.split(e,[]))}}class Q extends z{constructor(e,t=Ia(e)){super(),this.text=e,this.length=t}get lines(){return this.text.length}get children(){return null}lineInner(e,t,i,s){for(let r=0;;r++){let o=this.text[r],l=s+o.length;if((t?i:l)>=e)return new Na(s,l,i,o);s=l+1,i++}}decompose(e,t,i,s){let r=e<=0&&t>=this.length?this:new Q(hr(this.text,e,t),Math.min(t,this.length)-Math.max(0,e));if(s&1){let o=i.pop(),l=Pi(r.text,o.text.slice(),0,r.length);if(l.length<=32)i.push(new Q(l,o.length+r.length));else{let h=l.length>>1;i.push(new Q(l.slice(0,h)),new Q(l.slice(h)))}}else i.push(r)}replace(e,t,i){if(!(i instanceof Q))return super.replace(e,t,i);let s=Pi(this.text,Pi(i.text,hr(this.text,0,e)),t),r=this.length+i.length-(t-e);return s.length<=32?new Q(s,r):Ve.from(Q.split(s,[]),r)}sliceString(e,t=this.length,i=`
import{S as Sa,i as va,s as Ca,e as Aa,f as Ma,g as Da,y as xn,o as Oa,G as Ta,H as Ba,I as Ra,J as Pa,K as La,C as kn,L as Ea}from"./index.0d48f812.js";class z{constructor(){}lineAt(e){if(e<0||e>this.length)throw new RangeError(`Invalid position ${e} in document of length ${this.length}`);return this.lineInner(e,!1,1,0)}line(e){if(e<1||e>this.lines)throw new RangeError(`Invalid line number ${e} in ${this.lines}-line document`);return this.lineInner(e,!0,1,0)}replace(e,t,i){let s=[];return this.decompose(0,e,s,2),i.length&&i.decompose(0,i.length,s,3),this.decompose(t,this.length,s,1),Ve.from(s,this.length-(t-e)+i.length)}append(e){return this.replace(this.length,this.length,e)}slice(e,t=this.length){let i=[];return this.decompose(e,t,i,0),Ve.from(i,t-e)}eq(e){if(e==this)return!0;if(e.length!=this.length||e.lines!=this.lines)return!1;let t=this.scanIdentical(e,1),i=this.length-this.scanIdentical(e,-1),s=new Zt(this),r=new Zt(e);for(let o=t,l=t;;){if(s.next(o),r.next(o),o=0,s.lineBreak!=r.lineBreak||s.done!=r.done||s.value!=r.value)return!1;if(l+=s.value.length,s.done||l>=i)return!0}}iter(e=1){return new Zt(this,e)}iterRange(e,t=this.length){return new zo(this,e,t)}iterLines(e,t){let i;if(e==null)i=this.iter();else{t==null&&(t=this.lines+1);let s=this.line(e).from;i=this.iterRange(s,Math.max(s,t==this.lines+1?this.length:t<=1?0:this.line(t-1).to))}return new qo(i)}toString(){return this.sliceString(0)}toJSON(){let e=[];return this.flatten(e),e}static of(e){if(e.length==0)throw new RangeError("A document must have at least one line");return e.length==1&&!e[0]?z.empty:e.length<=32?new Q(e):Ve.from(Q.split(e,[]))}}class Q extends z{constructor(e,t=Ia(e)){super(),this.text=e,this.length=t}get lines(){return this.text.length}get children(){return null}lineInner(e,t,i,s){for(let r=0;;r++){let o=this.text[r],l=s+o.length;if((t?i:l)>=e)return new Na(s,l,i,o);s=l+1,i++}}decompose(e,t,i,s){let r=e<=0&&t>=this.length?this:new Q(hr(this.text,e,t),Math.min(t,this.length)-Math.max(0,e));if(s&1){let o=i.pop(),l=Pi(r.text,o.text.slice(),0,r.length);if(l.length<=32)i.push(new Q(l,o.length+r.length));else{let h=l.length>>1;i.push(new Q(l.slice(0,h)),new Q(l.slice(h)))}}else i.push(r)}replace(e,t,i){if(!(i instanceof Q))return super.replace(e,t,i);let s=Pi(this.text,Pi(i.text,hr(this.text,0,e)),t),r=this.length+i.length-(t-e);return s.length<=32?new Q(s,r):Ve.from(Q.split(s,[]),r)}sliceString(e,t=this.length,i=`
`){let s="";for(let r=0,o=0;r<=t&&o<this.text.length;o++){let l=this.text[o],h=r+l.length;r>e&&o&&(s+=i),e<h&&t>r&&(s+=l.slice(Math.max(0,e-r),t-r)),r=h+1}return s}flatten(e){for(let t of this.text)e.push(t)}scanIdentical(){return 0}static split(e,t){let i=[],s=-1;for(let r of e)i.push(r),s+=r.length+1,i.length==32&&(t.push(new Q(i,s)),i=[],s=-1);return s>-1&&t.push(new Q(i,s)),t}}class Ve extends z{constructor(e,t){super(),this.children=e,this.length=t,this.lines=0;for(let i of e)this.lines+=i.lines}lineInner(e,t,i,s){for(let r=0;;r++){let o=this.children[r],l=s+o.length,h=i+o.lines-1;if((t?h:l)>=e)return o.lineInner(e,t,i,s);s=l+1,i=h+1}}decompose(e,t,i,s){for(let r=0,o=0;o<=t&&r<this.children.length;r++){let l=this.children[r],h=o+l.length;if(e<=h&&t>=o){let a=s&((o<=e?1:0)|(h>=t?2:0));o>=e&&h<=t&&!a?i.push(l):l.decompose(e-o,t-o,i,a)}o=h+1}}replace(e,t,i){if(i.lines<this.lines)for(let s=0,r=0;s<this.children.length;s++){let o=this.children[s],l=r+o.length;if(e>=r&&t<=l){let h=o.replace(e-r,t-r,i),a=this.lines-o.lines+h.lines;if(h.lines<a>>5-1&&h.lines>a>>5+1){let c=this.children.slice();return c[s]=h,new Ve(c,this.length-(t-e)+i.length)}return super.replace(r,l,h)}r=l+1}return super.replace(e,t,i)}sliceString(e,t=this.length,i=`
`){let s="";for(let r=0,o=0;r<this.children.length&&o<=t;r++){let l=this.children[r],h=o+l.length;o>e&&r&&(s+=i),e<h&&t>o&&(s+=l.sliceString(e-o,t-o,i)),o=h+1}return s}flatten(e){for(let t of this.children)t.flatten(e)}scanIdentical(e,t){if(!(e instanceof Ve))return 0;let i=0,[s,r,o,l]=t>0?[0,0,this.children.length,e.children.length]:[this.children.length-1,e.children.length-1,-1,-1];for(;;s+=t,r+=t){if(s==o||r==l)return i;let h=this.children[s],a=e.children[r];if(h!=a)return i+h.scanIdentical(a,t);i+=h.length+1}}static from(e,t=e.reduce((i,s)=>i+s.length+1,-1)){let i=0;for(let d of e)i+=d.lines;if(i<32){let d=[];for(let p of e)p.flatten(d);return new Q(d,t)}let s=Math.max(32,i>>5),r=s<<1,o=s>>1,l=[],h=0,a=-1,c=[];function f(d){let p;if(d.lines>r&&d instanceof Ve)for(let g of d.children)f(g);else d.lines>o&&(h>o||!h)?(u(),l.push(d)):d instanceof Q&&h&&(p=c[c.length-1])instanceof Q&&d.lines+p.lines<=32?(h+=d.lines,a+=d.length+1,c[c.length-1]=new Q(p.text.concat(d.text),p.length+1+d.length)):(h+d.lines>s&&u(),h+=d.lines,a+=d.length+1,c.push(d))}function u(){h!=0&&(l.push(c.length==1?c[0]:Ve.from(c,a)),a=-1,h=c.length=0)}for(let d of e)f(d);return u(),l.length==1?l[0]:new Ve(l,t)}}z.empty=new Q([""],0);function Ia(n){let e=-1;for(let t of n)e+=t.length+1;return e}function Pi(n,e,t=0,i=1e9){for(let s=0,r=0,o=!0;r<n.length&&s<=i;r++){let l=n[r],h=s+l.length;h>=t&&(h>i&&(l=l.slice(0,i-s)),s<t&&(l=l.slice(t-s)),o?(e[e.length-1]+=l,o=!1):e.push(l)),s=h+1}return e}function hr(n,e,t){return Pi(n,[""],e,t)}class Zt{constructor(e,t=1){this.dir=t,this.done=!1,this.lineBreak=!1,this.value="",this.nodes=[e],this.offsets=[t>0?1:(e instanceof Q?e.text.length:e.children.length)<<1]}nextInner(e,t){for(this.done=this.lineBreak=!1;;){let i=this.nodes.length-1,s=this.nodes[i],r=this.offsets[i],o=r>>1,l=s instanceof Q?s.text.length:s.children.length;if(o==(t>0?l:0)){if(i==0)return this.done=!0,this.value="",this;t>0&&this.offsets[i-1]++,this.nodes.pop(),this.offsets.pop()}else if((r&1)==(t>0?0:1)){if(this.offsets[i]+=t,e==0)return this.lineBreak=!0,this.value=`
`,this;e--}else if(s instanceof Q){let h=s.text[o+(t<0?-1:0)];if(this.offsets[i]+=t,h.length>Math.max(0,e))return this.value=e==0?h:t>0?h.slice(e):h.slice(0,h.length-e),this;e-=h.length}else{let h=s.children[o+(t<0?-1:0)];e>h.length?(e-=h.length,this.offsets[i]+=t):(t<0&&this.offsets[i]--,this.nodes.push(h),this.offsets.push(t>0?1:(h instanceof Q?h.text.length:h.children.length)<<1))}}}next(e=0){return e<0&&(this.nextInner(-e,-this.dir),e=this.value.length),this.nextInner(e,this.dir)}}class zo{constructor(e,t,i){this.value="",this.done=!1,this.cursor=new Zt(e,t>i?-1:1),this.pos=t>i?e.length:0,this.from=Math.min(t,i),this.to=Math.max(t,i)}nextInner(e,t){if(t<0?this.pos<=this.from:this.pos>=this.to)return this.value="",this.done=!0,this;e+=Math.max(0,t<0?this.pos-this.to:this.from-this.pos);let i=t<0?this.pos-this.from:this.to-this.pos;e>i&&(e=i),i-=e;let{value:s}=this.cursor.next(e);return this.pos+=(s.length+e)*t,this.value=s.length<=i?s:t<0?s.slice(s.length-i):s.slice(0,i),this.done=!this.value,this}next(e=0){return e<0?e=Math.max(e,this.from-this.pos):e>0&&(e=Math.min(e,this.to-this.pos)),this.nextInner(e,this.cursor.dir)}get lineBreak(){return this.cursor.lineBreak&&this.value!=""}}class qo{constructor(e){this.inner=e,this.afterBreak=!0,this.value="",this.done=!1}next(e=0){let{done:t,lineBreak:i,value:s}=this.inner.next(e);return t?(this.done=!0,this.value=""):i?this.afterBreak?this.value="":(this.afterBreak=!0,this.next()):(this.value=s,this.afterBreak=!1),this}get lineBreak(){return!1}}typeof Symbol!="undefined"&&(z.prototype[Symbol.iterator]=function(){return this.iter()},Zt.prototype[Symbol.iterator]=zo.prototype[Symbol.iterator]=qo.prototype[Symbol.iterator]=function(){return this});class Na{constructor(e,t,i,s){this.from=e,this.to=t,this.number=i,this.text=s}get length(){return this.to-this.from}}let Dt="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(n=>n?parseInt(n,36):1);for(let n=1;n<Dt.length;n++)Dt[n]+=Dt[n-1];function Va(n){for(let e=1;e<Dt.length;e+=2)if(Dt[e]>n)return Dt[e-1]<=n;return!1}function ar(n){return n>=127462&&n<=127487}const cr=8205;function Ae(n,e,t=!0,i=!0){return(t?Ko:Fa)(n,e,i)}function Ko(n,e,t){if(e==n.length)return e;e&&Uo(n.charCodeAt(e))&&jo(n.charCodeAt(e-1))&&e--;let i=re(n,e);for(e+=ve(i);e<n.length;){let s=re(n,e);if(i==cr||s==cr||t&&Va(s))e+=ve(s),i=s;else if(ar(s)){let r=0,o=e-2;for(;o>=0&&ar(re(n,o));)r++,o-=2;if(r%2==0)break;e+=2}else break}return e}function Fa(n,e,t){for(;e>0;){let i=Ko(n,e-2,t);if(i<e)return i;e--}return 0}function Uo(n){return n>=56320&&n<57344}function jo(n){return n>=55296&&n<56320}function re(n,e){let t=n.charCodeAt(e);if(!jo(t)||e+1==n.length)return t;let i=n.charCodeAt(e+1);return Uo(i)?(t-55296<<10)+(i-56320)+65536:t}function Rs(n){return n<=65535?String.fromCharCode(n):(n-=65536,String.fromCharCode((n>>10)+55296,(n&1023)+56320))}function ve(n){return n<65536?1:2}const Kn=/\r\n?|\n/;var me=function(n){return n[n.Simple=0]="Simple",n[n.TrackDel=1]="TrackDel",n[n.TrackBefore=2]="TrackBefore",n[n.TrackAfter=3]="TrackAfter",n}(me||(me={}));class We{constructor(e){this.sections=e}get length(){let e=0;for(let t=0;t<this.sections.length;t+=2)e+=this.sections[t];return e}get newLength(){let e=0;for(let t=0;t<this.sections.length;t+=2){let i=this.sections[t+1];e+=i<0?this.sections[t]:i}return e}get empty(){return this.sections.length==0||this.sections.length==2&&this.sections[1]<0}iterGaps(e){for(let t=0,i=0,s=0;t<this.sections.length;){let r=this.sections[t++],o=this.sections[t++];o<0?(e(i,s,r),s+=r):s+=o,i+=r}}iterChangedRanges(e,t=!1){Un(this,e,t)}get invertedDesc(){let e=[];for(let t=0;t<this.sections.length;){let i=this.sections[t++],s=this.sections[t++];s<0?e.push(i,s) Line truncated
`}get readOnly(){return this.facet(tl)}phrase(e,...t){for(let i of this.facet(V.phrases))if(Object.prototype.hasOwnProperty.call(i,e)){e=i[e];break}return t.length&&(e=e.replace(/\$(\$|\d*)/g,(i,s)=>{if(s=="$")return"$";let r=+(s||1);return!r||r>t.length?i:t[r-1]})),e}languageDataAt(e,t,i=-1){let s=[];for(let r of this.facet(Xo))for(let o of r(this,t,i))Object.prototype.hasOwnProperty.call(o,e)&&s.push(o[e]);return s}charCategorizer(e){return Xa(this.languageDataAt("wordChars",e).join(""))}wordAt(e){let{text:t,from:i,length:s}=this.doc.lineAt(e),r=this.charCategorizer(e),o=e-i,l=e-i;for(;o>0;){let h=Ae(t,o,!1);if(r(t.slice(h,o))!=ce.Word)break;o=h}for(;l<s;){let h=Ae(t,l);if(r(t.slice(l,h))!=ce.Word)break;l=h}return o==l?null:m.range(o+i,l+i)}}V.allowMultipleSelections=Yo;V.tabSize=T.define({combine:n=>n.length?n[0]:4});V.lineSeparator=_o;V.readOnly=tl;V.phrases=T.define({compare(n,e){let t=Object.keys(n),i=Object.keys(e);return t.length==i.length&&t.every(s=>n[s]==e[s])}});V.languageData=Xo;V.changeFilter=Qo;V.transactionFilter=Zo;V.transactionExtender=el;_e.reconfigure=H.define();function zt(n,e,t={}){let i={};for(let s of n)for(let r of Object.keys(s)){let o=s[r],l=i[r];if(l===void 0)i[r]=o;else if(!(l===o||o===void 0))if(Object.hasOwnProperty.call(t,r))i[r]=t[r](l,o);else throw new Error("Config merge conflict for field "+r)}for(let s in e)i[s]===void 0&&(i[s]=e[s]);return i}class pt{eq(e){return this==e}range(e,t=e){return ri.create(e,t,this)}}pt.prototype.startSide=pt.prototype.endSide=0;pt.prototype.point=!1;pt.prototype.mapMode=me.TrackDel;class ri{constructor(e,t,i){this.from=e,this.to=t,this.value=i}static create(e,t,i){return new ri(e,t,i)}}function Yn(n,e){return n.from-e.from||n.value.startSide-e.value.startSide}class Es{constructor(e,t,i,s){this.from=e,this.to=t,this.value=i,this.maxPoint=s}get length(){return this.to[this.to.length-1]}findIndex(e,t,i,s=0){let r=i?this.to:this.from;for(let o=s,l=r.length;;){if(o==l)return o;let h=o+l>>1,a=r[h]-e||(i?this.value[h].endSide:this.value[h].startSide)-t;if(h==o)return a>=0?o:l;a>=0?l=h:o=h+1}}between(e,t,i,s){for(let r=this.findIndex(t,-1e9,!0),o=this.findIndex(i,1e9,!1,r);r<o;r++)if(s(this.from[r]+e,this.to[r]+e,this.value[r])===!1)return!1}map(e,t){let i=[],s=[],r=[],o=-1,l=-1;for(let h=0;h<this.value.length;h++){let a=this.value[h],c=this.from[h]+e,f=this.to[h]+e,u,d;if(c==f){let p=t.mapPos(c,a.startSide,a.mapMode);if(p==null||(u=d=p,a.startSide!=a.endSide&&(d=t.mapPos(c,a.endSide),d<u)))continue}else if(u=t.mapPos(c,a.startSide),d=t.mapPos(f,a.endSide),u>d||u==d&&a.startSide>0&&a.endSide<=0)continue;(d-u||a.endSide-a.startSide)<0||(o<0&&(o=u),a.point&&(l=Math.max(l,d-u)),i.push(a),s.push(u-o),r.push(d-o))}return{mapped:i.length?new Es(s,r,i,l):null,pos:o}}}class Y{constructor(e,t,i,s){this.chunkPos=e,this.chunk=t,this.nextLayer=i,this.maxPoint=s}static create(e,t,i,s){return new Y(e,t,i,s)}get length(){let e=this.chunk.length-1;return e<0?0:Math.max(this.chunkEnd(e),this.nextLayer.length)}get size(){if(this.isEmpty)return 0;let e=this.nextLayer.size;for(let t of this.chunk)e+=t.value.length;return e}chunkEnd(e){return this.chunkPos[e]+this.chunk[e].length}update(e){let{add:t=[],sort:i=!1,filterFrom:s=0,filterTo:r=this.length}=e,o=e.filter;if(t.length==0&&!o)return this;if(i&&(t=t.slice().sort(Yn)),this.isEmpty)return t.length?Y.of(t):this;let l=new sl(this,null,-1).goto(0),h=0,a=[],c=new gt;for(;l.value||h<t.length;)if(h<t.length&&(l.from-t[h].from||l.startSide-t[h].value.startSide)>=0){let f=t[h++];c.addInner(f.from,f.to,f.value)||a.push(f)}else l.rangeIndex==1&&l.chunkIndex<this.chunk.length&&(h==t.length||this.chunkEnd(l.chunkIndex)<t[h].from)&&(!o||s>this.chunkEnd(l.chunkIndex)||r<this.chunkPos[l.chunkIndex])&&c.addChunk(this.chunkPos[l.chunkIndex],this.chunk[l.chunkIndex])?l.nextChunk():((!o||s>l.to||r<l.from||o(l.from,l.to,l.value))&&(c.addInner(l.from,l.to,l.value)||a.push(ri.create(l.from,l.to,l.value))),l.next());return c.finishInner(this.nextLayer.isEmpty&&!a.length?Y.empty:this.nextLayer.update({add:a,filter:o,filterFrom:s,filterTo:r}))}map(e){if(e.empty||this.isEmpty)return this;let t=[],i=[],s=-1;for(let o=0;o<this.chunk.length;o++){let l=this.chunkPos[o],h=this.chunk[o],a=e.touchesRange(l,l+h.length);if(a===!1)s=Math.max(s,h.maxPoint),t.push(h),i.push(e.mapPos(l));else if(a===!0){let{mapped:c,pos:f}=h.map(l,e);c&&(s=Math.max(s,c.maxPoint),t.push(c),i.push(f))}}let r=this.nextLayer.map(e);return t.length==0?r:new Y(i,t,r||Y.empty,s)}between(e,t,i){if(!this.isEmpty){for(let s=0;s<this.chunk.length;s++){let r=this.chunkPos[s],o=this.chunk[s];if(t>=r&&e<=r+o.length&&o.between(r,e-r,t-r,i)===!1)return}this.nextLayer.between(e,t,i)}}iter(e=0){return oi.from([this]).goto(e)}get isEmpty(){return this.nextLayer==this}static iter(e,t=0){return oi.from(e).goto(t)}static compare(e,t,i,s,r=-1){let o=e.filter(f=>f.maxPoint>0||!f.isEmpty&&f.maxPoint>=r),l=t.filter(f=>f.maxPoint>0||!f.isEmpty&&f.maxPoint>=r),h=dr(o,l,i),a=new Ut(o,h,r),c=new Ut(l Line truncated
`)}static newName(){let e=yr[mr]||1;return yr[mr]=e+1,Zn+e.toString(36)}static mount(e,t){(e[es]||new _a(e)).mount(Array.isArray(t)?t:[t])}}let bi=null;class _a{constructor(e){if(!e.head&&e.adoptedStyleSheets&&typeof CSSStyleSheet!="undefined"){if(bi)return e.adoptedStyleSheets=[bi.sheet].concat(e.adoptedStyleSheets),e[es]=bi;this.sheet=new CSSStyleSheet,e.adoptedStyleSheets=[this.sheet].concat(e.adoptedStyleSheets),bi=this}else{this.styleTag=(e.ownerDocument||e).createElement("style");let t=e.head||e;t.insertBefore(this.styleTag,t.firstChild)}this.modules=[],e[es]=this}mount(e){let t=this.sheet,i=0,s=0;for(let r=0;r<e.length;r++){let o=e[r],l=this.modules.indexOf(o);if(l<s&&l>-1&&(this.modules.splice(l,1),s--,l=-1),l==-1){if(this.modules.splice(s++,0,o),t)for(let h=0;h<o.rules.length;h++)t.insertRule(o.rules[h],i++)}else{for(;s<l;)i+=this.modules[s++].rules.length;i+=o.rules.length,s++}}if(!t){let r="";for(let o=0;o<this.modules.length;o++)r+=this.modules[o].getRules()+`
`;this.styleTag.textContent=r}}}var rt={8:"Backspace",9:"Tab",10:"Enter",12:"NumLock",13:"Enter",16:"Shift",17:"Control",18:"Alt",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",44:"PrintScreen",45:"Insert",46:"Delete",59:";",61:"=",91:"Meta",92:"Meta",106:"*",107:"+",108:",",109:"-",110:".",111:"/",144:"NumLock",145:"ScrollLock",160:"Shift",161:"Shift",162:"Control",163:"Control",164:"Alt",165:"Alt",173:"-",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'",229:"q"},Rt={48:")",49:"!",50:"@",51:"#",52:"$",53:"%",54:"^",55:"&",56:"*",57:"(",59:":",61:"+",173:"_",186:":",187:"+",188:"<",189:"_",190:">",191:"?",192:"~",219:"{",220:"|",221:"}",222:'"',229:"Q"},br=typeof navigator!="undefined"&&/Chrome\/(\d+)/.exec(navigator.userAgent),Qa=typeof navigator!="undefined"&&/Apple Computer/.test(navigator.vendor),Za=typeof navigator!="undefined"&&/Gecko\/\d+/.test(navigator.userAgent),wr=typeof navigator!="undefined"&&/Mac/.test(navigator.platform),ec=typeof navigator!="undefined"&&/MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent),tc=br&&(wr||+br[1]<57)||Za&&wr;for(var oe=0;oe<10;oe++)rt[48+oe]=rt[96+oe]=String(oe);for(var oe=1;oe<=24;oe++)rt[oe+111]="F"+oe;for(var oe=65;oe<=90;oe++)rt[oe]=String.fromCharCode(oe+32),Rt[oe]=String.fromCharCode(oe);for(var vn in rt)Rt.hasOwnProperty(vn)||(Rt[vn]=rt[vn]);function ic(n){var e=tc&&(n.ctrlKey||n.altKey||n.metaKey)||(Qa||ec)&&n.shiftKey&&n.key&&n.key.length==1,t=!e&&n.key||(n.shiftKey?Rt:rt)[n.keyCode]||n.key||"Unidentified";return t=="Esc"&&(t="Escape"),t=="Del"&&(t="Delete"),t=="Left"&&(t="ArrowLeft"),t=="Up"&&(t="ArrowUp"),t=="Right"&&(t="ArrowRight"),t=="Down"&&(t="ArrowDown"),t}function Hi(n){let e;return n.nodeType==11?e=n.getSelection?n:n.ownerDocument:e=n,e.getSelection()}function Pt(n,e){return e?n==e||n.contains(e.nodeType!=1?e.parentNode:e):!1}function nc(){let n=document.activeElement;for(;n&&n.shadowRoot;)n=n.shadowRoot.activeElement;return n}function ts(n,e){if(!e.anchorNode)return!1;try{return Pt(n,e.anchorNode)}catch{return!1}}function li(n){return n.nodeType==3?Lt(n,0,n.nodeValue.length).getClientRects():n.nodeType==1?n.getClientRects():[]}function Wi(n,e,t,i){return t?xr(n,e,t,i,-1)||xr(n,e,t,i,1):!1}function is(n){for(var e=0;;e++)if(n=n.previousSibling,!n)return e}function xr(n,e,t,i,s){for(;;){if(n==t&&e==i)return!0;if(e==(s<0?0:zi(n))){if(n.nodeName=="DIV")return!1;let r=n.parentNode;if(!r||r.nodeType!=1)return!1;e=is(n)+(s<0?0:1),n=r}else if(n.nodeType==1){if(n=n.childNodes[e+(s<0?-1:0)],n.nodeType==1&&n.contentEditable=="false")return!1;e=s<0?zi(n):0}else return!1}}function zi(n){return n.nodeType==3?n.nodeValue.length:n.childNodes.length}const rl={left:0,right:0,top:0,bottom:0};function on(n,e){let t=e?n.left:n.right;return{left:t,right:t,top:n.top,bottom:n.bottom}}function sc(n){return{left:0,right:n.innerWidth,top:0,bottom:n.innerHeight}}function rc(n,e,t,i,s,r,o,l){let h=n.ownerDocument,a=h.defaultView;for(let c=n;c;)if(c.nodeType==1){let f,u=c==h.body;if(u)f=sc(a);else{if(c.scrollHeight<=c.clientHeight&&c.scrollWidth<=c.clientWidth){c=c.parentNode;continue}let g=c.getBoundingClientRect();f={left:g.left,right:g.left+c.clientWidth,top:g.top,bottom:g.top+c.clientHeight}}let d=0,p=0;if(s=="nearest")e.top<f.top?(p=-(f.top-e.top+o),t>0&&e.bottom>f.bottom+p&&(p=e.bottom-f.bottom+p+o)):e.bottom>f.bottom&&(p=e.bottom-f.bottom+o,t<0&&e.top-p<f.top&&(p=-(f.top+p-e.top+o)));else{let g=e.bottom-e.top,y=f.bottom-f.top;p=(s=="center"&&g<=y?e.top+g/2-y/2:s=="start"||s=="center"&&t<0?e.top-o:e.bottom-y+o)-f.top}if(i=="nearest"?e.left<f.left?(d=-(f.left-e.left+r),t>0&&e.right>f.right+d&&(d=e.right-f.right+d+r)):e.right>f.right&&(d=e.right-f.right+r,t<0&&e.left<f.left+d&&(d=-(f.left+d-e.left+r))):d=(i=="center"?e.left+(e.right-e.left)/2-(f.right-f.left)/2:i=="start"==l?e.left-r:e.right-(f.right-f.left)+r)-f.left,d||p)if(u)a.scrollBy(d,p);else{if(p){let g=c.scrollTop;c.scrollTop+=p,p=c.scrollTop-g}if(d){let g=c.scrollLeft;c.scrollLeft+=d,d=c.scrollLeft-g}e={left:e.left-d,top:e.top-p,right:e.right-d,bottom:e.bottom-p}}if(u)break;c=c.assignedSlot||c.parentNode,i=s="nearest"}else if(c.nodeType==11)c=c.host;else break}class oc{constructor(){this.anchorNode=null,this.anchorOffset=0,this.focusNode=null,this.focusOffset=0}eq(e){return this.anchorNode==e.anchorNode&&this.anchorOffset==e.anchorOffset&&this.focusNode==e.focusNode&&this.focusOffset==e.focusOffset}setRange(e){this.set(e.anchorNode,e.anchorOffset,e.focusNode,e.focusOffset)}set(e,t,i,s){this.anchorNode=e,this.anchorOffset=t,this.focusNode=i,this.focusOffset=s}}let vt=null;function ol(n){if(n.setActive)return n.setActive();if(vt)return n.focus(vt);let e=[];for(let t=n;t&&(e.push(t,t.scrollTop,t.scrollLeft),t!=t.ownerDocument);t=t.parentNode);if(n.focus(vt==null?{get preventScroll(){return vt={preventScroll:!0},!0}}:void 0),!vt){vt=!1;for(let t=0;t<e.length;){let iLine truncated
`,this;e--}else if(s instanceof Q){let h=s.text[o+(t<0?-1:0)];if(this.offsets[i]+=t,h.length>Math.max(0,e))return this.value=e==0?h:t>0?h.slice(e):h.slice(0,h.length-e),this;e-=h.length}else{let h=s.children[o+(t<0?-1:0)];e>h.length?(e-=h.length,this.offsets[i]+=t):(t<0&&this.offsets[i]--,this.nodes.push(h),this.offsets.push(t>0?1:(h instanceof Q?h.text.length:h.children.length)<<1))}}}next(e=0){return e<0&&(this.nextInner(-e,-this.dir),e=this.value.length),this.nextInner(e,this.dir)}}class zo{constructor(e,t,i){this.value="",this.done=!1,this.cursor=new Zt(e,t>i?-1:1),this.pos=t>i?e.length:0,this.from=Math.min(t,i),this.to=Math.max(t,i)}nextInner(e,t){if(t<0?this.pos<=this.from:this.pos>=this.to)return this.value="",this.done=!0,this;e+=Math.max(0,t<0?this.pos-this.to:this.from-this.pos);let i=t<0?this.pos-this.from:this.to-this.pos;e>i&&(e=i),i-=e;let{value:s}=this.cursor.next(e);return this.pos+=(s.length+e)*t,this.value=s.length<=i?s:t<0?s.slice(s.length-i):s.slice(0,i),this.done=!this.value,this}next(e=0){return e<0?e=Math.max(e,this.from-this.pos):e>0&&(e=Math.min(e,this.to-this.pos)),this.nextInner(e,this.cursor.dir)}get lineBreak(){return this.cursor.lineBreak&&this.value!=""}}class qo{constructor(e){this.inner=e,this.afterBreak=!0,this.value="",this.done=!1}next(e=0){let{done:t,lineBreak:i,value:s}=this.inner.next(e);return t?(this.done=!0,this.value=""):i?this.afterBreak?this.value="":(this.afterBreak=!0,this.next()):(this.value=s,this.afterBreak=!1),this}get lineBreak(){return!1}}typeof Symbol<"u"&&(z.prototype[Symbol.iterator]=function(){return this.iter()},Zt.prototype[Symbol.iterator]=zo.prototype[Symbol.iterator]=qo.prototype[Symbol.iterator]=function(){return this});class Na{constructor(e,t,i,s){this.from=e,this.to=t,this.number=i,this.text=s}get length(){return this.to-this.from}}let Dt="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(n=>n?parseInt(n,36):1);for(let n=1;n<Dt.length;n++)Dt[n]+=Dt[n-1];function Va(n){for(let e=1;e<Dt.length;e+=2)if(Dt[e]>n)return Dt[e-1]<=n;return!1}function ar(n){return n>=127462&&n<=127487}const cr=8205;function Ae(n,e,t=!0,i=!0){return(t?Ko:Fa)(n,e,i)}function Ko(n,e,t){if(e==n.length)return e;e&&Uo(n.charCodeAt(e))&&jo(n.charCodeAt(e-1))&&e--;let i=re(n,e);for(e+=ve(i);e<n.length;){let s=re(n,e);if(i==cr||s==cr||t&&Va(s))e+=ve(s),i=s;else if(ar(s)){let r=0,o=e-2;for(;o>=0&&ar(re(n,o));)r++,o-=2;if(r%2==0)break;e+=2}else break}return e}function Fa(n,e,t){for(;e>0;){let i=Ko(n,e-2,t);if(i<e)return i;e--}return 0}function Uo(n){return n>=56320&&n<57344}function jo(n){return n>=55296&&n<56320}function re(n,e){let t=n.charCodeAt(e);if(!jo(t)||e+1==n.length)return t;let i=n.charCodeAt(e+1);return Uo(i)?(t-55296<<10)+(i-56320)+65536:t}function Rs(n){return n<=65535?String.fromCharCode(n):(n-=65536,String.fromCharCode((n>>10)+55296,(n&1023)+56320))}function ve(n){return n<65536?1:2}const Kn=/\r\n?|\n/;var me=function(n){return n[n.Simple=0]="Simple",n[n.TrackDel=1]="TrackDel",n[n.TrackBefore=2]="TrackBefore",n[n.TrackAfter=3]="TrackAfter",n}(me||(me={}));class We{constructor(e){this.sections=e}get length(){let e=0;for(let t=0;t<this.sections.length;t+=2)e+=this.sections[t];return e}get newLength(){let e=0;for(let t=0;t<this.sections.length;t+=2){let i=this.sections[t+1];e+=i<0?this.sections[t]:i}return e}get empty(){return this.sections.length==0||this.sections.length==2&&this.sections[1]<0}iterGaps(e){for(let t=0,i=0,s=0;t<this.sections.length;){let r=this.sections[t++],o=this.sections[t++];o<0?(e(i,s,r),s+=r):s+=o,i+=r}}iterChangedRanges(e,t=!1){Un(this,e,t)}get invertedDesc(){let e=[];for(let t=0;t<this.sections.length;){let i=this.sections[t++],s=this.sections[t++];s<0?e.push(i,s):e.push(s Line truncated
`}get readOnly(){return this.facet(tl)}phrase(e,...t){for(let i of this.facet(V.phrases))if(Object.prototype.hasOwnProperty.call(i,e)){e=i[e];break}return t.length&&(e=e.replace(/\$(\$|\d*)/g,(i,s)=>{if(s=="$")return"$";let r=+(s||1);return!r||r>t.length?i:t[r-1]})),e}languageDataAt(e,t,i=-1){let s=[];for(let r of this.facet(Xo))for(let o of r(this,t,i))Object.prototype.hasOwnProperty.call(o,e)&&s.push(o[e]);return s}charCategorizer(e){return Xa(this.languageDataAt("wordChars",e).join(""))}wordAt(e){let{text:t,from:i,length:s}=this.doc.lineAt(e),r=this.charCategorizer(e),o=e-i,l=e-i;for(;o>0;){let h=Ae(t,o,!1);if(r(t.slice(h,o))!=ce.Word)break;o=h}for(;l<s;){let h=Ae(t,l);if(r(t.slice(l,h))!=ce.Word)break;l=h}return o==l?null:m.range(o+i,l+i)}}V.allowMultipleSelections=Yo;V.tabSize=T.define({combine:n=>n.length?n[0]:4});V.lineSeparator=_o;V.readOnly=tl;V.phrases=T.define({compare(n,e){let t=Object.keys(n),i=Object.keys(e);return t.length==i.length&&t.every(s=>n[s]==e[s])}});V.languageData=Xo;V.changeFilter=Qo;V.transactionFilter=Zo;V.transactionExtender=el;_e.reconfigure=H.define();function zt(n,e,t={}){let i={};for(let s of n)for(let r of Object.keys(s)){let o=s[r],l=i[r];if(l===void 0)i[r]=o;else if(!(l===o||o===void 0))if(Object.hasOwnProperty.call(t,r))i[r]=t[r](l,o);else throw new Error("Config merge conflict for field "+r)}for(let s in e)i[s]===void 0&&(i[s]=e[s]);return i}class pt{eq(e){return this==e}range(e,t=e){return ri.create(e,t,this)}}pt.prototype.startSide=pt.prototype.endSide=0;pt.prototype.point=!1;pt.prototype.mapMode=me.TrackDel;class ri{constructor(e,t,i){this.from=e,this.to=t,this.value=i}static create(e,t,i){return new ri(e,t,i)}}function Yn(n,e){return n.from-e.from||n.value.startSide-e.value.startSide}class Es{constructor(e,t,i,s){this.from=e,this.to=t,this.value=i,this.maxPoint=s}get length(){return this.to[this.to.length-1]}findIndex(e,t,i,s=0){let r=i?this.to:this.from;for(let o=s,l=r.length;;){if(o==l)return o;let h=o+l>>1,a=r[h]-e||(i?this.value[h].endSide:this.value[h].startSide)-t;if(h==o)return a>=0?o:l;a>=0?l=h:o=h+1}}between(e,t,i,s){for(let r=this.findIndex(t,-1e9,!0),o=this.findIndex(i,1e9,!1,r);r<o;r++)if(s(this.from[r]+e,this.to[r]+e,this.value[r])===!1)return!1}map(e,t){let i=[],s=[],r=[],o=-1,l=-1;for(let h=0;h<this.value.length;h++){let a=this.value[h],c=this.from[h]+e,f=this.to[h]+e,u,d;if(c==f){let p=t.mapPos(c,a.startSide,a.mapMode);if(p==null||(u=d=p,a.startSide!=a.endSide&&(d=t.mapPos(c,a.endSide),d<u)))continue}else if(u=t.mapPos(c,a.startSide),d=t.mapPos(f,a.endSide),u>d||u==d&&a.startSide>0&&a.endSide<=0)continue;(d-u||a.endSide-a.startSide)<0||(o<0&&(o=u),a.point&&(l=Math.max(l,d-u)),i.push(a),s.push(u-o),r.push(d-o))}return{mapped:i.length?new Es(s,r,i,l):null,pos:o}}}class Y{constructor(e,t,i,s){this.chunkPos=e,this.chunk=t,this.nextLayer=i,this.maxPoint=s}static create(e,t,i,s){return new Y(e,t,i,s)}get length(){let e=this.chunk.length-1;return e<0?0:Math.max(this.chunkEnd(e),this.nextLayer.length)}get size(){if(this.isEmpty)return 0;let e=this.nextLayer.size;for(let t of this.chunk)e+=t.value.length;return e}chunkEnd(e){return this.chunkPos[e]+this.chunk[e].length}update(e){let{add:t=[],sort:i=!1,filterFrom:s=0,filterTo:r=this.length}=e,o=e.filter;if(t.length==0&&!o)return this;if(i&&(t=t.slice().sort(Yn)),this.isEmpty)return t.length?Y.of(t):this;let l=new sl(this,null,-1).goto(0),h=0,a=[],c=new gt;for(;l.value||h<t.length;)if(h<t.length&&(l.from-t[h].from||l.startSide-t[h].value.startSide)>=0){let f=t[h++];c.addInner(f.from,f.to,f.value)||a.push(f)}else l.rangeIndex==1&&l.chunkIndex<this.chunk.length&&(h==t.length||this.chunkEnd(l.chunkIndex)<t[h].from)&&(!o||s>this.chunkEnd(l.chunkIndex)||r<this.chunkPos[l.chunkIndex])&&c.addChunk(this.chunkPos[l.chunkIndex],this.chunk[l.chunkIndex])?l.nextChunk():((!o||s>l.to||r<l.from||o(l.from,l.to,l.value))&&(c.addInner(l.from,l.to,l.value)||a.push(ri.create(l.from,l.to,l.value))),l.next());return c.finishInner(this.nextLayer.isEmpty&&!a.length?Y.empty:this.nextLayer.update({add:a,filter:o,filterFrom:s,filterTo:r}))}map(e){if(e.empty||this.isEmpty)return this;let t=[],i=[],s=-1;for(let o=0;o<this.chunk.length;o++){let l=this.chunkPos[o],h=this.chunk[o],a=e.touchesRange(l,l+h.length);if(a===!1)s=Math.max(s,h.maxPoint),t.push(h),i.push(e.mapPos(l));else if(a===!0){let{mapped:c,pos:f}=h.map(l,e);c&&(s=Math.max(s,c.maxPoint),t.push(c),i.push(f))}}let r=this.nextLayer.map(e);return t.length==0?r:new Y(i,t,r||Y.empty,s)}between(e,t,i){if(!this.isEmpty){for(let s=0;s<this.chunk.length;s++){let r=this.chunkPos[s],o=this.chunk[s];if(t>=r&&e<=r+o.length&&o.between(r,e-r,t-r,i)===!1)return}this.nextLayer.between(e,t,i)}}iter(e=0){return oi.from([this]).goto(e)}get isEmpty(){return this.nextLayer==this}static iter(e,t=0){return oi.from(e).goto(t)}static compare(e,t,i,s,r=-1){let o=e.filter(f=>f.maxPoint>0||!f.isEmpty&&f.maxPoint>=r),l=t.filter(f=>f.maxPoint>0||!f.isEmpty&&f.maxPoint>=r),h=dr(o,l,i),a=new Ut(o,h,r),c=new Ut(l Line truncated
`)}static newName(){let e=yr[mr]||1;return yr[mr]=e+1,Zn+e.toString(36)}static mount(e,t){(e[es]||new _a(e)).mount(Array.isArray(t)?t:[t])}}let bi=null;class _a{constructor(e){if(!e.head&&e.adoptedStyleSheets&&typeof CSSStyleSheet<"u"){if(bi)return e.adoptedStyleSheets=[bi.sheet].concat(e.adoptedStyleSheets),e[es]=bi;this.sheet=new CSSStyleSheet,e.adoptedStyleSheets=[this.sheet].concat(e.adoptedStyleSheets),bi=this}else{this.styleTag=(e.ownerDocument||e).createElement("style");let t=e.head||e;t.insertBefore(this.styleTag,t.firstChild)}this.modules=[],e[es]=this}mount(e){let t=this.sheet,i=0,s=0;for(let r=0;r<e.length;r++){let o=e[r],l=this.modules.indexOf(o);if(l<s&&l>-1&&(this.modules.splice(l,1),s--,l=-1),l==-1){if(this.modules.splice(s++,0,o),t)for(let h=0;h<o.rules.length;h++)t.insertRule(o.rules[h],i++)}else{for(;s<l;)i+=this.modules[s++].rules.length;i+=o.rules.length,s++}}if(!t){let r="";for(let o=0;o<this.modules.length;o++)r+=this.modules[o].getRules()+`
`;this.styleTag.textContent=r}}}var rt={8:"Backspace",9:"Tab",10:"Enter",12:"NumLock",13:"Enter",16:"Shift",17:"Control",18:"Alt",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",44:"PrintScreen",45:"Insert",46:"Delete",59:";",61:"=",91:"Meta",92:"Meta",106:"*",107:"+",108:",",109:"-",110:".",111:"/",144:"NumLock",145:"ScrollLock",160:"Shift",161:"Shift",162:"Control",163:"Control",164:"Alt",165:"Alt",173:"-",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'",229:"q"},Rt={48:")",49:"!",50:"@",51:"#",52:"$",53:"%",54:"^",55:"&",56:"*",57:"(",59:":",61:"+",173:"_",186:":",187:"+",188:"<",189:"_",190:">",191:"?",192:"~",219:"{",220:"|",221:"}",222:'"',229:"Q"},br=typeof navigator<"u"&&/Chrome\/(\d+)/.exec(navigator.userAgent),Qa=typeof navigator<"u"&&/Apple Computer/.test(navigator.vendor),Za=typeof navigator<"u"&&/Gecko\/\d+/.test(navigator.userAgent),wr=typeof navigator<"u"&&/Mac/.test(navigator.platform),ec=typeof navigator<"u"&&/MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent),tc=br&&(wr||+br[1]<57)||Za&&wr;for(var oe=0;oe<10;oe++)rt[48+oe]=rt[96+oe]=String(oe);for(var oe=1;oe<=24;oe++)rt[oe+111]="F"+oe;for(var oe=65;oe<=90;oe++)rt[oe]=String.fromCharCode(oe+32),Rt[oe]=String.fromCharCode(oe);for(var vn in rt)Rt.hasOwnProperty(vn)||(Rt[vn]=rt[vn]);function ic(n){var e=tc&&(n.ctrlKey||n.altKey||n.metaKey)||(Qa||ec)&&n.shiftKey&&n.key&&n.key.length==1,t=!e&&n.key||(n.shiftKey?Rt:rt)[n.keyCode]||n.key||"Unidentified";return t=="Esc"&&(t="Escape"),t=="Del"&&(t="Delete"),t=="Left"&&(t="ArrowLeft"),t=="Up"&&(t="ArrowUp"),t=="Right"&&(t="ArrowRight"),t=="Down"&&(t="ArrowDown"),t}function Hi(n){let e;return n.nodeType==11?e=n.getSelection?n:n.ownerDocument:e=n,e.getSelection()}function Pt(n,e){return e?n==e||n.contains(e.nodeType!=1?e.parentNode:e):!1}function nc(){let n=document.activeElement;for(;n&&n.shadowRoot;)n=n.shadowRoot.activeElement;return n}function ts(n,e){if(!e.anchorNode)return!1;try{return Pt(n,e.anchorNode)}catch{return!1}}function li(n){return n.nodeType==3?Lt(n,0,n.nodeValue.length).getClientRects():n.nodeType==1?n.getClientRects():[]}function Wi(n,e,t,i){return t?xr(n,e,t,i,-1)||xr(n,e,t,i,1):!1}function is(n){for(var e=0;;e++)if(n=n.previousSibling,!n)return e}function xr(n,e,t,i,s){for(;;){if(n==t&&e==i)return!0;if(e==(s<0?0:zi(n))){if(n.nodeName=="DIV")return!1;let r=n.parentNode;if(!r||r.nodeType!=1)return!1;e=is(n)+(s<0?0:1),n=r}else if(n.nodeType==1){if(n=n.childNodes[e+(s<0?-1:0)],n.nodeType==1&&n.contentEditable=="false")return!1;e=s<0?zi(n):0}else return!1}}function zi(n){return n.nodeType==3?n.nodeValue.length:n.childNodes.length}const rl={left:0,right:0,top:0,bottom:0};function on(n,e){let t=e?n.left:n.right;return{left:t,right:t,top:n.top,bottom:n.bottom}}function sc(n){return{left:0,right:n.innerWidth,top:0,bottom:n.innerHeight}}function rc(n,e,t,i,s,r,o,l){let h=n.ownerDocument,a=h.defaultView;for(let c=n;c;)if(c.nodeType==1){let f,u=c==h.body;if(u)f=sc(a);else{if(c.scrollHeight<=c.clientHeight&&c.scrollWidth<=c.clientWidth){c=c.parentNode;continue}let g=c.getBoundingClientRect();f={left:g.left,right:g.left+c.clientWidth,top:g.top,bottom:g.top+c.clientHeight}}let d=0,p=0;if(s=="nearest")e.top<f.top?(p=-(f.top-e.top+o),t>0&&e.bottom>f.bottom+p&&(p=e.bottom-f.bottom+p+o)):e.bottom>f.bottom&&(p=e.bottom-f.bottom+o,t<0&&e.top-p<f.top&&(p=-(f.top+p-e.top+o)));else{let g=e.bottom-e.top,y=f.bottom-f.top;p=(s=="center"&&g<=y?e.top+g/2-y/2:s=="start"||s=="center"&&t<0?e.top-o:e.bottom-y+o)-f.top}if(i=="nearest"?e.left<f.left?(d=-(f.left-e.left+r),t>0&&e.right>f.right+d&&(d=e.right-f.right+d+r)):e.right>f.right&&(d=e.right-f.right+r,t<0&&e.left<f.left+d&&(d=-(f.left+d-e.left+r))):d=(i=="center"?e.left+(e.right-e.left)/2-(f.right-f.left)/2:i=="start"==l?e.left-r:e.right-(f.right-f.left)+r)-f.left,d||p)if(u)a.scrollBy(d,p);else{if(p){let g=c.scrollTop;c.scrollTop+=p,p=c.scrollTop-g}if(d){let g=c.scrollLeft;c.scrollLeft+=d,d=c.scrollLeft-g}e={left:e.left-d,top:e.top-p,right:e.right-d,bottom:e.bottom-p}}if(u)break;c=c.assignedSlot||c.parentNode,i=s="nearest"}else if(c.nodeType==11)c=c.host;else break}class oc{constructor(){this.anchorNode=null,this.anchorOffset=0,this.focusNode=null,this.focusOffset=0}eq(e){return this.anchorNode==e.anchorNode&&this.anchorOffset==e.anchorOffset&&this.focusNode==e.focusNode&&this.focusOffset==e.focusOffset}setRange(e){this.set(e.anchorNode,e.anchorOffset,e.focusNode,e.focusOffset)}set(e,t,i,s){this.anchorNode=e,this.anchorOffset=t,this.focusNode=i,this.focusOffset=s}}let vt=null;function ol(n){if(n.setActive)return n.setActive();if(vt)return n.focus(vt);let e=[];for(let t=n;t&&(e.push(t,t.scrollTop,t.scrollLeft),t!=t.ownerDocument);t=t.parentNode);if(n.focus(vt==null?{get preventScroll(){return vt={preventScroll:!0},!0}}:void 0),!vt){vt=!1;for(let t=0;t<e.length;){let i=e[t++],s=e[t++],r=e[t++];i.scrollTop!=s&&(i.Line truncated
`,s=n.state.doc.line(s.number+(t?1:-1)),r=n.bidiSpans(s),a=m.cursor(t?s.from:s.to)}if(h){if(!h(c))return l}else{if(!i)return a;h=i(c)}l=a}}function Pc(n,e,t){let i=n.state.charCategorizer(e),s=i(t);return r=>{let o=i(r);return s==ce.Space&&(s=o),s==o}}function Lc(n,e,t,i){let s=e.head,r=t?1:-1;if(s==(t?n.state.doc.length:0))return m.cursor(s,e.assoc);let o=e.goalColumn,l,h=n.contentDOM.getBoundingClientRect(),a=n.coordsAtPos(s),c=n.documentTop;if(a)o==null&&(o=a.left-h.left),l=r<0?a.top:a.bottom;else{let d=n.viewState.lineBlockAt(s);o==null&&(o=Math.min(h.right-h.left,n.defaultCharacterWidth*(s-d.from))),l=(r<0?d.top:d.bottom)+c}let f=h.left+o,u=i!=null?i:n.defaultLineHeight>>1;for(let d=0;;d+=10){let p=l+(u+d)*r,g=Il(n,{x:f,y:p},!1,r);if(p<h.top||p>h.bottom||(r<0?g<s:g>s))return m.cursor(g,e.assoc,void 0,o)}}function Dn(n,e,t){let i=n.state.facet(Dl).map(s=>s(n));for(;;){let s=!1;for(let r of i)r.between(t.from-1,t.from+1,(o,l,h)=>{t.from>o&&t.from<l&&(t=e.from>t.from?m.cursor(o,1):m.cursor(l,-1),s=!0)});if(!s)return t}}class Ec{constructor(e){this.lastKeyCode=0,this.lastKeyTime=0,this.chromeScrollHack=-1,this.pendingIOSKey=void 0,this.lastSelectionOrigin=null,this.lastSelectionTime=0,this.lastEscPress=0,this.lastContextMenu=0,this.scrollHandlers=[],this.registeredEvents=[],this.customHandlers=[],this.composing=-1,this.compositionFirstChange=null,this.compositionEndedAt=0,this.rapidCompositionStart=!1,this.mouseSelection=null;for(let t in ie){let i=ie[t];e.contentDOM.addEventListener(t,s=>{!Vr(e,s)||this.ignoreDuringComposition(s)||t=="keydown"&&this.keydown(e,s)||(this.mustFlushObserver(s)&&e.observer.forceFlush(),this.runCustomHandlers(t,e,s)?s.preventDefault():i(e,s))}),this.registeredEvents.push(t)}M.chrome&&M.chrome_version>=102&&e.scrollDOM.addEventListener("wheel",()=>{this.chromeScrollHack<0?e.contentDOM.style.pointerEvents="none":window.clearTimeout(this.chromeScrollHack),this.chromeScrollHack=setTimeout(()=>{this.chromeScrollHack=-1,e.contentDOM.style.pointerEvents=""},100)},{passive:!0}),this.notifiedFocused=e.hasFocus,M.safari&&e.contentDOM.addEventListener("input",()=>null)}setSelectionOrigin(e){this.lastSelectionOrigin=e,this.lastSelectionTime=Date.now()}ensureHandlers(e,t){var i;let s;this.customHandlers=[];for(let r of t)if(s=(i=r.update(e).spec)===null||i===void 0?void 0:i.domEventHandlers){this.customHandlers.push({plugin:r.value,handlers:s});for(let o in s)this.registeredEvents.indexOf(o)<0&&o!="scroll"&&(this.registeredEvents.push(o),e.contentDOM.addEventListener(o,l=>{!Vr(e,l)||this.runCustomHandlers(o,e,l)&&l.preventDefault()}))}}runCustomHandlers(e,t,i){for(let s of this.customHandlers){let r=s.handlers[e];if(r)try{if(r.call(s.plugin,i,t)||i.defaultPrevented)return!0}catch(o){Re(t.state,o)}}return!1}runScrollHandlers(e,t){for(let i of this.customHandlers){let s=i.handlers.scroll;if(s)try{s.call(i.plugin,t,e)}catch(r){Re(e.state,r)}}}keydown(e,t){if(this.lastKeyCode=t.keyCode,this.lastKeyTime=Date.now(),t.keyCode==9&&Date.now()<this.lastEscPress+2e3)return!0;if(M.android&&M.chrome&&!t.synthetic&&(t.keyCode==13||t.keyCode==8))return e.observer.delayAndroidKey(t.key,t.keyCode),!0;let i;return M.ios&&(i=Nl.find(s=>s.keyCode==t.keyCode))&&!(t.ctrlKey||t.altKey||t.metaKey)&&!t.synthetic?(this.pendingIOSKey=i,setTimeout(()=>this.flushIOSKey(e),250),!0):!1}flushIOSKey(e){let t=this.pendingIOSKey;return t?(this.pendingIOSKey=void 0,ti(e.contentDOM,t.key,t.keyCode)):!1}ignoreDuringComposition(e){return/^key/.test(e.type)?this.composing>0?!0:M.safari&&Date.now()-this.compositionEndedAt<100?(this.compositionEndedAt=0,!0):!1:!1}mustFlushObserver(e){return e.type=="keydown"&&e.keyCode!=229||e.type=="compositionend"&&!M.ios}startMouseSelection(e){this.mouseSelection&&this.mouseSelection.destroy(),this.mouseSelection=e}update(e){this.mouseSelection&&this.mouseSelection.update(e),e.transactions.length&&(this.lastKeyCode=this.lastSelectionTime=0)}destroy(){this.mouseSelection&&this.mouseSelection.destroy()}}const Nl=[{key:"Backspace",keyCode:8,inputType:"deleteContentBackward"},{key:"Enter",keyCode:13,inputType:"insertParagraph"},{key:"Delete",keyCode:46,inputType:"deleteContentForward"}],Vl=[16,17,18,20,91,92,224,225];class Ic{constructor(e,t,i,s){this.view=e,this.style=i,this.mustSelect=s,this.lastEvent=t;let r=e.contentDOM.ownerDocument;r.addEventListener("mousemove",this.move=this.move.bind(this)),r.addEventListener("mouseup",this.up=this.up.bind(this)),this.extend=t.shiftKey,this.multiple=e.state.facet(V.allowMultipleSelections)&&Nc(e,t),this.dragMove=Vc(e,t),this.dragging=Fc(e,t)&&Fs(t)==1?null:!1,this.dragging===!1&&(t.preventDefault(),this.select(t))}move(e){if(e.buttons==0)return this.destroy();this.dragging===!1&&this.select(this.lastEvent=e)}up(e){this.dragging==null&&this.select(this.lastEvent),this.dragging||e.preventDefault(),this.destroy()}destroy(){let e=this.view.contentDOM.ownerDocument;e.removeEventListener("mousemove",this.move),e.removeEventListener("mouseup",this.up),this.view.i Line truncated
-\x7F-\x9F\xAD\u061C\u200B\u200E\u200F\u2028\u2029\u202D\u202E\uFEFF\uFFF9-\uFFFC]`,ws),Bf={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",8233:"paragraph separator",65279:"zero width no-break space",65532:"object replacement"};let Bn=null;function Rf(){var n;if(Bn==null&&typeof document!="undefined"&&document.body){let e=document.body.style;Bn=((n=e.tabSize)!==null&&n!==void 0?n:e.MozTabSize)!=null}return Bn||!1}const Ii=T.define({combine(n){let e=zt(n,{render:null,specialChars:Tf,addSpecialChars:null});return(e.replaceTabs=!Rf())&&(e.specialChars=new RegExp(" |"+e.specialChars.source,ws)),e.addSpecialChars&&(e.specialChars=new RegExp(e.specialChars.source+"|"+e.addSpecialChars.source,ws)),e}});function Pf(n={}){return[Ii.of(n),Lf()]}let so=null;function Lf(){return so||(so=ue.fromClass(class{constructor(n){this.view=n,this.decorations=R.none,this.decorationCache=Object.create(null),this.decorator=this.makeDecorator(n.state.facet(Ii)),this.decorations=this.decorator.createDeco(n)}makeDecorator(n){return new Of({regexp:n.specialChars,decoration:(e,t,i)=>{let{doc:s}=t.state,r=re(e[0],0);if(r==9){let o=s.lineAt(i),l=t.state.tabSize,h=ui(o.text,l,i-o.from);return R.replace({widget:new Vf((l-h%l)*this.view.defaultCharacterWidth)})}return this.decorationCache[r]||(this.decorationCache[r]=R.replace({widget:new Nf(n,r)}))},boundary:n.replaceTabs?void 0:/[^]/})}update(n){let e=n.state.facet(Ii);n.startState.facet(Ii)!=e?(this.decorator=this.makeDecorator(e),this.decorations=this.decorator.createDeco(n.view)):this.decorations=this.decorator.updateDeco(n,this.decorations)}},{decorations:n=>n.decorations}))}const Ef="\u2022";function If(n){return n>=32?Ef:n==10?"\u2424":String.fromCharCode(9216+n)}class Nf extends xt{constructor(e,t){super(),this.options=e,this.code=t}eq(e){return e.code==this.code}toDOM(e){let t=If(this.code),i=e.state.phrase("Control character")+" "+(Bf[this.code]||"0x"+this.code.toString(16)),s=this.options.render&&this.options.render(this.code,i,t);if(s)return s;let r=document.createElement("span");return r.textContent=t,r.title=i,r.setAttribute("aria-label",i),r.className="cm-specialChar",r}ignoreEvent(){return!1}}class Vf extends xt{constructor(e){super(),this.width=e}eq(e){return e.width==this.width}toDOM(){let e=document.createElement("span");return e.textContent=" ",e.className="cm-tab",e.style.width=this.width+"px",e}ignoreEvent(){return!1}}class Ff extends xt{constructor(e){super(),this.content=e}toDOM(){let e=document.createElement("span");return e.className="cm-placeholder",e.style.pointerEvents="none",e.appendChild(typeof this.content=="string"?document.createTextNode(this.content):this.content),typeof this.content=="string"?e.setAttribute("aria-label","placeholder "+this.content):e.setAttribute("aria-hidden","true"),e}ignoreEvent(){return!1}}function ro(n){return ue.fromClass(class{constructor(e){this.view=e,this.placeholder=R.set([R.widget({widget:new Ff(n),side:1}).range(0)])}get decorations(){return this.view.state.doc.length?R.none:this.placeholder}},{decorations:e=>e.decorations})}const xs=2e3;function Hf(n,e,t){let i=Math.min(e.line,t.line),s=Math.max(e.line,t.line),r=[];if(e.off>xs||t.off>xs||e.col<0||t.col<0){let o=Math.min(e.off,t.off),l=Math.max(e.off,t.off);for(let h=i;h<=s;h++){let a=n.doc.line(h);a.length<=l&&r.push(m.range(a.from+o,a.to+l))}}else{let o=Math.min(e.col,t.col),l=Math.max(e.col,t.col);for(let h=i;h<=s;h++){let a=n.doc.line(h),c=Qn(a.text,o,n.tabSize,!0);if(c>-1){let f=Qn(a.text,l,n.tabSize);r.push(m.range(a.from+c,a.from+f))}}}return r}function Wf(n,e){let t=n.coordsAtPos(n.viewport.from);return t?Math.round(Math.abs((t.left-e)/n.defaultCharacterWidth)):-1}function oo(n,e){let t=n.posAtCoords({x:e.clientX,y:e.clientY},!1),i=n.state.doc.lineAt(t),s=t-i.from,r=s>xs?-1:s==i.length?Wf(n,e.clientX):ui(i.text,n.state.tabSize,t-i.from);return{line:i.number,col:r,off:s}}function zf(n,e){let t=oo(n,e),i=n.state.selection;return t?{update(s){if(s.docChanged){let r=s.changes.mapPos(s.startState.doc.line(t.line).from),o=s.state.doc.lineAt(r);t={line:o.number,col:t.col,off:Math.min(t.off,o.length)},i=i.map(s.changes)}},get(s,r,o){let l=oo(n,s);if(!l)return i;let h=Hf(n.state,t,l);return h.length?o?m.create(h.concat(i.ranges)):m.create(h):i}}:null}function qf(n){let e=(n==null?void 0:n.eventFilter)||(t=>t.altKey&&t.button==0);return O.mouseSelectionStyle.of((t,i)=>e(i)?zf(t,i):null)}const Rn="-10000px";class Kf{constructor(e,t,i){this.facet=t,this.createTooltipView=i,this.input=e.state.facet(t),this.tooltips=this.input.filter(s=>s),this.tooltipViews=this.tooltips.map(i)}update(e){let t=e.state.facet(this.facet),i=t.filter(r=>r);if(t===this.input){for(let r of this.tooltipViews)r.update&&r.update(e);return!1}let s=[] Line truncated
-\x7F-\x9F\xAD\u061C\u200B\u200E\u200F\u2028\u2029\u202D\u202E\uFEFF\uFFF9-\uFFFC]`,ws),Bf={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",8233:"paragraph separator",65279:"zero width no-break space",65532:"object replacement"};let Bn=null;function Rf(){var n;if(Bn==null&&typeof document<"u"&&document.body){let e=document.body.style;Bn=((n=e.tabSize)!==null&&n!==void 0?n:e.MozTabSize)!=null}return Bn||!1}const Ii=T.define({combine(n){let e=zt(n,{render:null,specialChars:Tf,addSpecialChars:null});return(e.replaceTabs=!Rf())&&(e.specialChars=new RegExp(" |"+e.specialChars.source,ws)),e.addSpecialChars&&(e.specialChars=new RegExp(e.specialChars.source+"|"+e.addSpecialChars.source,ws)),e}});function Pf(n={}){return[Ii.of(n),Lf()]}let so=null;function Lf(){return so||(so=ue.fromClass(class{constructor(n){this.view=n,this.decorations=R.none,this.decorationCache=Object.create(null),this.decorator=this.makeDecorator(n.state.facet(Ii)),this.decorations=this.decorator.createDeco(n)}makeDecorator(n){return new Of({regexp:n.specialChars,decoration:(e,t,i)=>{let{doc:s}=t.state,r=re(e[0],0);if(r==9){let o=s.lineAt(i),l=t.state.tabSize,h=ui(o.text,l,i-o.from);return R.replace({widget:new Vf((l-h%l)*this.view.defaultCharacterWidth)})}return this.decorationCache[r]||(this.decorationCache[r]=R.replace({widget:new Nf(n,r)}))},boundary:n.replaceTabs?void 0:/[^]/})}update(n){let e=n.state.facet(Ii);n.startState.facet(Ii)!=e?(this.decorator=this.makeDecorator(e),this.decorations=this.decorator.createDeco(n.view)):this.decorations=this.decorator.updateDeco(n,this.decorations)}},{decorations:n=>n.decorations}))}const Ef="\u2022";function If(n){return n>=32?Ef:n==10?"\u2424":String.fromCharCode(9216+n)}class Nf extends xt{constructor(e,t){super(),this.options=e,this.code=t}eq(e){return e.code==this.code}toDOM(e){let t=If(this.code),i=e.state.phrase("Control character")+" "+(Bf[this.code]||"0x"+this.code.toString(16)),s=this.options.render&&this.options.render(this.code,i,t);if(s)return s;let r=document.createElement("span");return r.textContent=t,r.title=i,r.setAttribute("aria-label",i),r.className="cm-specialChar",r}ignoreEvent(){return!1}}class Vf extends xt{constructor(e){super(),this.width=e}eq(e){return e.width==this.width}toDOM(){let e=document.createElement("span");return e.textContent=" ",e.className="cm-tab",e.style.width=this.width+"px",e}ignoreEvent(){return!1}}class Ff extends xt{constructor(e){super(),this.content=e}toDOM(){let e=document.createElement("span");return e.className="cm-placeholder",e.style.pointerEvents="none",e.appendChild(typeof this.content=="string"?document.createTextNode(this.content):this.content),typeof this.content=="string"?e.setAttribute("aria-label","placeholder "+this.content):e.setAttribute("aria-hidden","true"),e}ignoreEvent(){return!1}}function ro(n){return ue.fromClass(class{constructor(e){this.view=e,this.placeholder=R.set([R.widget({widget:new Ff(n),side:1}).range(0)])}get decorations(){return this.view.state.doc.length?R.none:this.placeholder}},{decorations:e=>e.decorations})}const xs=2e3;function Hf(n,e,t){let i=Math.min(e.line,t.line),s=Math.max(e.line,t.line),r=[];if(e.off>xs||t.off>xs||e.col<0||t.col<0){let o=Math.min(e.off,t.off),l=Math.max(e.off,t.off);for(let h=i;h<=s;h++){let a=n.doc.line(h);a.length<=l&&r.push(m.range(a.from+o,a.to+l))}}else{let o=Math.min(e.col,t.col),l=Math.max(e.col,t.col);for(let h=i;h<=s;h++){let a=n.doc.line(h),c=Qn(a.text,o,n.tabSize,!0);if(c>-1){let f=Qn(a.text,l,n.tabSize);r.push(m.range(a.from+c,a.from+f))}}}return r}function Wf(n,e){let t=n.coordsAtPos(n.viewport.from);return t?Math.round(Math.abs((t.left-e)/n.defaultCharacterWidth)):-1}function oo(n,e){let t=n.posAtCoords({x:e.clientX,y:e.clientY},!1),i=n.state.doc.lineAt(t),s=t-i.from,r=s>xs?-1:s==i.length?Wf(n,e.clientX):ui(i.text,n.state.tabSize,t-i.from);return{line:i.number,col:r,off:s}}function zf(n,e){let t=oo(n,e),i=n.state.selection;return t?{update(s){if(s.docChanged){let r=s.changes.mapPos(s.startState.doc.line(t.line).from),o=s.state.doc.lineAt(r);t={line:o.number,col:t.col,off:Math.min(t.off,o.length)},i=i.map(s.changes)}},get(s,r,o){let l=oo(n,s);if(!l)return i;let h=Hf(n.state,t,l);return h.length?o?m.create(h.concat(i.ranges)):m.create(h):i}}:null}function qf(n){let e=(n==null?void 0:n.eventFilter)||(t=>t.altKey&&t.button==0);return O.mouseSelectionStyle.of((t,i)=>e(i)?zf(t,i):null)}const Rn="-10000px";class Kf{constructor(e,t,i){this.facet=t,this.createTooltipView=i,this.input=e.state.facet(t),this.tooltips=this.input.filter(s=>s),this.tooltipViews=this.tooltips.map(i)}update(e){let t=e.state.facet(this.facet),i=t.filter(r=>r);if(t===this.input){for(let r of this.tooltipViews)r.update&&r.update(e);return!1}let s=[];for(let Line truncated
`&&(t="");else{let i=t.indexOf(`
`);i>-1&&(t=t.slice(0,i))}return e+t.length<=this.to?t:t.slice(0,this.to-e)}nextLine(){let e=this.parsedPos,t=this.lineAfter(e),i=e+t.length;for(let s=this.rangeIndex;;){let r=this.ranges[s].to;if(r>=i||(t=t.slice(0,r-(i-t.length)),s++,s==this.ranges.length))break;let o=this.ranges[s].from,l=this.lineAfter(o);t+=l,i=o+l.length}return{line:t,end:i}}skipGapsTo(e,t,i){for(;;){let s=this.ranges[this.rangeIndex].to,r=e+t;if(i>0?s>r:s>=r)break;t+=this.ranges[++this.rangeIndex].from-s}return t}moveRangeIndex(){for(;this.ranges[this.rangeIndex].to<this.parsedPos;)this.rangeIndex++}emitToken(e,t,i,s,r){if(this.ranges.length>1){r=this.skipGapsTo(t,r,1),t+=r;let o=this.chunk.length;r=this.skipGapsTo(i,r,-1),i+=r,s+=this.chunk.length-o}return this.chunk.push(e,t,i,s),r}parseLine(e){let{line:t,end:i}=this.nextLine(),s=0,{streamParser:r}=this.lang,o=new bh(t,e?e.state.tabSize:4,e?bt(e.state):2);if(o.eol())r.blankLine(this.state,o.indentUnit);else for(;!o.eol();){let l=xh(r.token,o,this.state);if(l&&(s=this.emitToken(this.lang.tokenTable.resolve(l),this.parsedPos+o.start,this.parsedPos+o.pos,4,s)),o.start>1e4)break}this.parsedPos=i,this.moveRangeIndex(),this.parsedPos<this.to&&this.parsedPos++}finishChunk(){let e=_.build({buffer:this.chunk,start:this.chunkStart,length:this.parsedPos-this.chunkStart,nodeSet:Wu,topID:0,maxBufferLength:2048,reused:this.chunkReused});e=new _(e.type,e.children,e.positions,e.length,[[this.lang.stateAfter,this.lang.streamParser.copyState(this.state)]]),this.chunks.push(e),this.chunkPos.push(this.chunkStart-this.ranges[0].from),this.chunk=[],this.chunkReused=void 0,this.chunkStart=this.parsedPos}finish(){return new _(this.lang.topNode,this.chunks,this.chunkPos,this.parsedPos-this.ranges[0].from).balance()}}function xh(n,e,t){e.start=e.pos;for(let i=0;i<10;i++){let s=n(e,t);if(e.pos>e.start)return s}throw new Error("Stream parser failed to advance stream.")}const Xs=Object.create(null),ci=[we.none],Wu=new zs(ci),yo=[],kh=Object.create(null);for(let[n,e]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"]])kh[n]=vh(Xs,e);class Sh{constructor(e){this.extra=e,this.table=Object.assign(Object.create(null),kh)}resolve(e){return e?this.table[e]||(this.table[e]=vh(this.extra,e)):0}}const zu=new Sh(Xs);function Fn(n,e){yo.indexOf(n)>-1||(yo.push(n),console.warn(e))}function vh(n,e){let t=null;for(let r of e.split(".")){let o=n[r]||k[r];o?typeof o=="function"?t?t=o(t):Fn(r,`Modifier ${r} used at start of tag`):t?Fn(r,`Tag ${r} used as modifier`):t=o:Fn(r,`Unknown highlighting tag ${r}`)}if(!t)return 0;let i=e.replace(/ /g,"_"),s=we.define({id:ci.length,name:i,props:[lu({[i]:t})]});return ci.push(s),s.id}function qu(n){let e=we.define({id:ci.length,name:"Document",props:[ai.add(()=>n)]});return ci.push(e),e}const Ku=n=>{let e=_s(n.state);return e.line?Uu(n):e.block?Gu(n):!1};function Ys(n,e){return({state:t,dispatch:i})=>{if(t.readOnly)return!1;let s=n(e,t);return s?(i(t.update(s)),!0):!1}}const Uu=Ys(Xu,0),ju=Ys(Ch,0),Gu=Ys((n,e)=>Ch(n,e,$u(e)),0);function _s(n,e=n.selection.main.head){let t=n.languageDataAt("commentTokens",e);return t.length?t[0]:{}}const Jt=50;function Ju(n,{open:e,close:t},i,s){let r=n.sliceDoc(i-Jt,i),o=n.sliceDoc(s,s+Jt),l=/\s*$/.exec(r)[0].length,h=/^\s*/.exec(o)[0].length,a=r.length-l;if(r.slice(a-e.length,a)==e&&o.slice(h,h+t.length)==t)return{open:{pos:i-l,margin:l&&1},close:{pos:s+h,margin:h&&1}};let c,f;s-i<=2*Jt?c=f=n.sliceDoc(i,s):(c=n.sliceDoc(i,i+Jt),f=n.sliceDoc(s-Jt,s));let u=/^\s*/.exec(c)[0].length,d=/\s*$/.exec(f)[0].length,p=f.length-d-t.length;return c.slice(u,u+e.length)==e&&f.slice(p,p+t.length)==t?{open:{pos:i+u+e.length,margin:/\s/.test(c.charAt(u+e.length))?1:0},close:{pos:s-d-t.length,margin:/\s/.test(f.charAt(p-1))?1:0}}:null}function $u(n){let e=[];for(let t of n.selection.ranges){let i=n.doc.lineAt(t.from),s=t.to<=i.to?i:n.doc.lineAt(t.to),r=e.length-1;r>=0&&e[r].to>i.from?e[r].to=s.to:e.push({from:i.from,to:s.to})}return e}function Ch(n,e,t=e.selection.ranges){let i=t.map(r=>_s(e,r.from).block);if(!i.every(r=>r))return null;let s=t.map((r,o)=>Ju(e,i[o],r.from,r.to));if(n!=2&&!s.every(r=>r))return{changes:e.changes(t.map((r,o)=>s[o]?[]:[{from:r.from,insert:i[o].open+" "},{from:r.to,insert:" "+i[o].close}]))};if(n!=1&&s.some(r=>r)){let r=[];for(let o=0,l;o<s.length;o++)if(l=s[o]){let h=i[o],{open:a,close:c}=l;r.push({from:a.pos-h.open.length,to:a.pos+a.margin},{from:c.pos-c.margin,to:c.pos+h.close.length})}return{changes:r}}return null}function Xu(n,e,t=e.selection.ranges){let i=[],s=-1;for(let{from:r,to:o}of t){let l=i.length,h=1e9;for(let a=r;a<=o;){let c=e.doc.lineAt(a);if(c.from>s&&(r==o||o>c.from)){s=c.from;let f=_s(e,a).line;if(!f)continue;let u=/^\s*/.exec(c.text)[0].length,d=u==c Line truncated
`:i=="r"?"\r":i=="t"?" ":"\\")}eq(e){return this.search==e.search&&this.replace==e.replace&&this.caseSensitive==e.caseSensitive&&this.regexp==e.regexp}create(){return this.regexp?new op(this):new rp(this)}getCursor(e,t=0,i=e.length){return this.regexp?Mt(this,e,t,i):At(this,e,t,i)}}class la{constructor(e){this.spec=e}}function At(n,e,t,i){return new Ht(e,n.unquoted,t,i,n.caseSensitive?void 0:s=>s.toLowerCase())}class rp extends la{constructor(e){super(e)}nextMatch(e,t,i){let s=At(this.spec,e,i,e.length).nextOverlapping();return s.done&&(s=At(this.spec,e,0,t).nextOverlapping()),s.done?null:s.value}prevMatchInRange(e,t,i){for(let s=i;;){let r=Math.max(t,s-1e4-this.spec.unquoted.length),o=At(this.spec,e,r,s),l=null;for(;!o.nextOverlapping().done;)l=o.value;if(l)return l;if(r==t)return null;s-=1e4}}prevMatch(e,t,i){return this.prevMatchInRange(e,0,t)||this.prevMatchInRange(e,i,e.length)}getReplacement(e){return this.spec.replace}matchAll(e,t){let i=At(this.spec,e,0,e.length),s=[];for(;!i.next().done;){if(s.length>=t)return null;s.push(i.value)}return s}highlight(e,t,i,s){let r=At(this.spec,e,Math.max(0,t-this.spec.unquoted.length),Math.min(i+this.spec.unquoted.length,e.length));for(;!r.next().done;)s(r.value.from,r.value.to)}}function Mt(n,e,t,i){return new na(e,n.search,n.caseSensitive?void 0:{ignoreCase:!0},t,i)}class op extends la{nextMatch(e,t,i){let s=Mt(this.spec,e,i,e.length).next();return s.done&&(s=Mt(this.spec,e,0,t).next()),s.done?null:s.value}prevMatchInRange(e,t,i){for(let s=1;;s++){let r=Math.max(t,i-s*1e4),o=Mt(this.spec,e,r,i),l=null;for(;!o.next().done;)l=o.value;if(l&&(r==t||l.from>r+10))return l;if(r==t)return null}}prevMatch(e,t,i){return this.prevMatchInRange(e,0,t)||this.prevMatchInRange(e,i,e.length)}getReplacement(e){return this.spec.replace.replace(/\$([$&\d+])/g,(t,i)=>i=="$"?"$":i=="&"?e.match[0]:i!="0"&&+i<e.match.length?e.match[i]:t)}matchAll(e,t){let i=Mt(this.spec,e,0,e.length),s=[];for(;!i.next().done;){if(s.length>=t)return null;s.push(i.value)}return s}highlight(e,t,i,s){let r=Mt(this.spec,e,Math.max(0,t-250),Math.min(i+250,e.length));for(;!r.next().done;)s(r.value.from,r.value.to)}}const fi=H.define(),tr=H.define(),it=ke.define({create(n){return new zn(Ts(n).create(),null)},update(n,e){for(let t of e.effects)t.is(fi)?n=new zn(t.value.create(),n.panel):t.is(tr)&&(n=new zn(n.query,t.value?ir:null));return n},provide:n=>Gi.from(n,e=>e.panel)});class zn{constructor(e,t){this.query=e,this.panel=t}}const lp=R.mark({class:"cm-searchMatch"}),hp=R.mark({class:"cm-searchMatch cm-searchMatch-selected"}),ap=ue.fromClass(class{constructor(n){this.view=n,this.decorations=this.highlight(n.state.field(it))}update(n){let e=n.state.field(it);(e!=n.startState.field(it)||n.docChanged||n.selectionSet||n.viewportChanged)&&(this.decorations=this.highlight(e))}highlight({query:n,panel:e}){if(!e||!n.spec.valid)return R.none;let{view:t}=this,i=new gt;for(let s=0,r=t.visibleRanges,o=r.length;s<o;s++){let{from:l,to:h}=r[s];for(;s<o-1&&h>r[s+1].from-2*250;)h=r[++s].to;n.highlight(t.state.doc,l,h,(a,c)=>{let f=t.state.selection.ranges.some(u=>u.from==a&&u.to==c);i.add(a,c,f?hp:lp)})}return i.finish()}},{decorations:n=>n.decorations});function pi(n){return e=>{let t=e.state.field(it,!1);return t&&t.query.spec.valid?n(e,t):ha(e)}}const en=pi((n,{query:e})=>{let{from:t,to:i}=n.state.selection.main,s=e.nextMatch(n.state.doc,t,i);return!s||s.from==t&&s.to==i?!1:(n.dispatch({selection:{anchor:s.from,head:s.to},scrollIntoView:!0,effects:nr(n,s),userEvent:"select.search"}),!0)}),tn=pi((n,{query:e})=>{let{state:t}=n,{from:i,to:s}=t.selection.main,r=e.prevMatch(t.doc,i,s);return r?(n.dispatch({selection:{anchor:r.from,head:r.to},scrollIntoView:!0,effects:nr(n,r),userEvent:"select.search"}),!0):!1}),cp=pi((n,{query:e})=>{let t=e.matchAll(n.state.doc,1e3);return!t||!t.length?!1:(n.dispatch({selection:m.create(t.map(i=>m.range(i.from,i.to))),userEvent:"select.search.matches"}),!0)}),fp=({state:n,dispatch:e})=>{let t=n.selection;if(t.ranges.length>1||t.main.empty)return!1;let{from:i,to:s}=t.main,r=[],o=0;for(let l=new Ht(n.doc,n.sliceDoc(i,s));!l.next().done;){if(r.length>1e3)return!1;l.value.from==i&&(o=r.length),r.push(m.range(l.value.from,l.value.to))}return e(n.update({selection:m.create(r,o),userEvent:"select.search.matches"})),!0},Po=pi((n,{query:e})=>{let{state:t}=n,{from:i,to:s}=t.selection.main;if(t.readOnly)return!1;let r=e.nextMatch(t.doc,i,i);if(!r)return!1;let o=[],l,h,a=[];if(r.from==i&&r.to==s&&(h=t.toText(e.getReplacement(r)),o.push({from:r.from,to:r.to,insert:h}),r=e.nextMatch(t.doc,r.from,r.to),a.push(O.announce.of(t.phrase("replaced match on line $",t.doc.lineAt(i).number)+"."))),r){let c=o.length==0||o[0].from>=r.to?0:r.to-r.from-h.length;l={anchor:r.from-c,head:r.to-c},a.push(nr(n,r))}return n.dispatch({changes:o,selection:l,scrollIntoView:!!l,effects:a,userEvent:"input.replace"}),!0}),up=pi((n,{query:e})=>{if(n.state.readOnly)return!1;let t=e.matchAll(n.state.doc,1e9).map(s=>{let{from:r,to:o}=s;re Line truncated
`);i>-1&&(t=t.slice(0,i))}return e+t.length<=this.to?t:t.slice(0,this.to-e)}nextLine(){let e=this.parsedPos,t=this.lineAfter(e),i=e+t.length;for(let s=this.rangeIndex;;){let r=this.ranges[s].to;if(r>=i||(t=t.slice(0,r-(i-t.length)),s++,s==this.ranges.length))break;let o=this.ranges[s].from,l=this.lineAfter(o);t+=l,i=o+l.length}return{line:t,end:i}}skipGapsTo(e,t,i){for(;;){let s=this.ranges[this.rangeIndex].to,r=e+t;if(i>0?s>r:s>=r)break;t+=this.ranges[++this.rangeIndex].from-s}return t}moveRangeIndex(){for(;this.ranges[this.rangeIndex].to<this.parsedPos;)this.rangeIndex++}emitToken(e,t,i,s,r){if(this.ranges.length>1){r=this.skipGapsTo(t,r,1),t+=r;let o=this.chunk.length;r=this.skipGapsTo(i,r,-1),i+=r,s+=this.chunk.length-o}return this.chunk.push(e,t,i,s),r}parseLine(e){let{line:t,end:i}=this.nextLine(),s=0,{streamParser:r}=this.lang,o=new bh(t,e?e.state.tabSize:4,e?bt(e.state):2);if(o.eol())r.blankLine(this.state,o.indentUnit);else for(;!o.eol();){let l=xh(r.token,o,this.state);if(l&&(s=this.emitToken(this.lang.tokenTable.resolve(l),this.parsedPos+o.start,this.parsedPos+o.pos,4,s)),o.start>1e4)break}this.parsedPos=i,this.moveRangeIndex(),this.parsedPos<this.to&&this.parsedPos++}finishChunk(){let e=_.build({buffer:this.chunk,start:this.chunkStart,length:this.parsedPos-this.chunkStart,nodeSet:Wu,topID:0,maxBufferLength:2048,reused:this.chunkReused});e=new _(e.type,e.children,e.positions,e.length,[[this.lang.stateAfter,this.lang.streamParser.copyState(this.state)]]),this.chunks.push(e),this.chunkPos.push(this.chunkStart-this.ranges[0].from),this.chunk=[],this.chunkReused=void 0,this.chunkStart=this.parsedPos}finish(){return new _(this.lang.topNode,this.chunks,this.chunkPos,this.parsedPos-this.ranges[0].from).balance()}}function xh(n,e,t){e.start=e.pos;for(let i=0;i<10;i++){let s=n(e,t);if(e.pos>e.start)return s}throw new Error("Stream parser failed to advance stream.")}const Xs=Object.create(null),ci=[we.none],Wu=new zs(ci),yo=[],kh=Object.create(null);for(let[n,e]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"]])kh[n]=vh(Xs,e);class Sh{constructor(e){this.extra=e,this.table=Object.assign(Object.create(null),kh)}resolve(e){return e?this.table[e]||(this.table[e]=vh(this.extra,e)):0}}const zu=new Sh(Xs);function Fn(n,e){yo.indexOf(n)>-1||(yo.push(n),console.warn(e))}function vh(n,e){let t=null;for(let r of e.split(".")){let o=n[r]||k[r];o?typeof o=="function"?t?t=o(t):Fn(r,`Modifier ${r} used at start of tag`):t?Fn(r,`Tag ${r} used as modifier`):t=o:Fn(r,`Unknown highlighting tag ${r}`)}if(!t)return 0;let i=e.replace(/ /g,"_"),s=we.define({id:ci.length,name:i,props:[lu({[i]:t})]});return ci.push(s),s.id}function qu(n){let e=we.define({id:ci.length,name:"Document",props:[ai.add(()=>n)]});return ci.push(e),e}const Ku=n=>{let e=_s(n.state);return e.line?Uu(n):e.block?Gu(n):!1};function Ys(n,e){return({state:t,dispatch:i})=>{if(t.readOnly)return!1;let s=n(e,t);return s?(i(t.update(s)),!0):!1}}const Uu=Ys(Xu,0),ju=Ys(Ch,0),Gu=Ys((n,e)=>Ch(n,e,$u(e)),0);function _s(n,e=n.selection.main.head){let t=n.languageDataAt("commentTokens",e);return t.length?t[0]:{}}const Jt=50;function Ju(n,{open:e,close:t},i,s){let r=n.sliceDoc(i-Jt,i),o=n.sliceDoc(s,s+Jt),l=/\s*$/.exec(r)[0].length,h=/^\s*/.exec(o)[0].length,a=r.length-l;if(r.slice(a-e.length,a)==e&&o.slice(h,h+t.length)==t)return{open:{pos:i-l,margin:l&&1},close:{pos:s+h,margin:h&&1}};let c,f;s-i<=2*Jt?c=f=n.sliceDoc(i,s):(c=n.sliceDoc(i,i+Jt),f=n.sliceDoc(s-Jt,s));let u=/^\s*/.exec(c)[0].length,d=/\s*$/.exec(f)[0].length,p=f.length-d-t.length;return c.slice(u,u+e.length)==e&&f.slice(p,p+t.length)==t?{open:{pos:i+u+e.length,margin:/\s/.test(c.charAt(u+e.length))?1:0},close:{pos:s-d-t.length,margin:/\s/.test(f.charAt(p-1))?1:0}}:null}function $u(n){let e=[];for(let t of n.selection.ranges){let i=n.doc.lineAt(t.from),s=t.to<=i.to?i:n.doc.lineAt(t.to),r=e.length-1;r>=0&&e[r].to>i.from?e[r].to=s.to:e.push({from:i.from,to:s.to})}return e}function Ch(n,e,t=e.selection.ranges){let i=t.map(r=>_s(e,r.from).block);if(!i.every(r=>r))return null;let s=t.map((r,o)=>Ju(e,i[o],r.from,r.to));if(n!=2&&!s.every(r=>r))return{changes:e.changes(t.map((r,o)=>s[o]?[]:[{from:r.from,insert:i[o].open+" "},{from:r.to,insert:" "+i[o].close}]))};if(n!=1&&s.some(r=>r)){let r=[];for(let o=0,l;o<s.length;o++)if(l=s[o]){let h=i[o],{open:a,close:c}=l;r.push({from:a.pos-h.open.length,to:a.pos+a.margin},{from:c.pos-c.margin,to:c.pos+h.close.length})}return{changes:r}}return null}function Xu(n,e,t=e.selection.ranges){let i=[],s=-1;for(let{from:r,to:o}of t){let l=i.length,h=1e9;for(let a=r;a<=o;){let c=e.doc.lineAt(a);if(c.from>s&&(r==o||o>c.from)){s=c.from;let f=_s(e,a).line;if(!f)continue;let u=/^\s*/.exec(c.text)[0].length,d=u==c Line truncated
`:i=="r"?"\r":i=="t"?" ":"\\")}eq(e){return this.search==e.search&&this.replace==e.replace&&this.caseSensitive==e.caseSensitive&&this.regexp==e.regexp}create(){return this.regexp?new op(this):new rp(this)}getCursor(e,t=0,i=e.length){return this.regexp?Mt(this,e,t,i):At(this,e,t,i)}}class la{constructor(e){this.spec=e}}function At(n,e,t,i){return new Ht(e,n.unquoted,t,i,n.caseSensitive?void 0:s=>s.toLowerCase())}class rp extends la{constructor(e){super(e)}nextMatch(e,t,i){let s=At(this.spec,e,i,e.length).nextOverlapping();return s.done&&(s=At(this.spec,e,0,t).nextOverlapping()),s.done?null:s.value}prevMatchInRange(e,t,i){for(let s=i;;){let r=Math.max(t,s-1e4-this.spec.unquoted.length),o=At(this.spec,e,r,s),l=null;for(;!o.nextOverlapping().done;)l=o.value;if(l)return l;if(r==t)return null;s-=1e4}}prevMatch(e,t,i){return this.prevMatchInRange(e,0,t)||this.prevMatchInRange(e,i,e.length)}getReplacement(e){return this.spec.replace}matchAll(e,t){let i=At(this.spec,e,0,e.length),s=[];for(;!i.next().done;){if(s.length>=t)return null;s.push(i.value)}return s}highlight(e,t,i,s){let r=At(this.spec,e,Math.max(0,t-this.spec.unquoted.length),Math.min(i+this.spec.unquoted.length,e.length));for(;!r.next().done;)s(r.value.from,r.value.to)}}function Mt(n,e,t,i){return new na(e,n.search,n.caseSensitive?void 0:{ignoreCase:!0},t,i)}class op extends la{nextMatch(e,t,i){let s=Mt(this.spec,e,i,e.length).next();return s.done&&(s=Mt(this.spec,e,0,t).next()),s.done?null:s.value}prevMatchInRange(e,t,i){for(let s=1;;s++){let r=Math.max(t,i-s*1e4),o=Mt(this.spec,e,r,i),l=null;for(;!o.next().done;)l=o.value;if(l&&(r==t||l.from>r+10))return l;if(r==t)return null}}prevMatch(e,t,i){return this.prevMatchInRange(e,0,t)||this.prevMatchInRange(e,i,e.length)}getReplacement(e){return this.spec.replace.replace(/\$([$&\d+])/g,(t,i)=>i=="$"?"$":i=="&"?e.match[0]:i!="0"&&+i<e.match.length?e.match[i]:t)}matchAll(e,t){let i=Mt(this.spec,e,0,e.length),s=[];for(;!i.next().done;){if(s.length>=t)return null;s.push(i.value)}return s}highlight(e,t,i,s){let r=Mt(this.spec,e,Math.max(0,t-250),Math.min(i+250,e.length));for(;!r.next().done;)s(r.value.from,r.value.to)}}const fi=H.define(),tr=H.define(),it=ke.define({create(n){return new zn(Ts(n).create(),null)},update(n,e){for(let t of e.effects)t.is(fi)?n=new zn(t.value.create(),n.panel):t.is(tr)&&(n=new zn(n.query,t.value?ir:null));return n},provide:n=>Gi.from(n,e=>e.panel)});class zn{constructor(e,t){this.query=e,this.panel=t}}const lp=R.mark({class:"cm-searchMatch"}),hp=R.mark({class:"cm-searchMatch cm-searchMatch-selected"}),ap=ue.fromClass(class{constructor(n){this.view=n,this.decorations=this.highlight(n.state.field(it))}update(n){let e=n.state.field(it);(e!=n.startState.field(it)||n.docChanged||n.selectionSet||n.viewportChanged)&&(this.decorations=this.highlight(e))}highlight({query:n,panel:e}){if(!e||!n.spec.valid)return R.none;let{view:t}=this,i=new gt;for(let s=0,r=t.visibleRanges,o=r.length;s<o;s++){let{from:l,to:h}=r[s];for(;s<o-1&&h>r[s+1].from-2*250;)h=r[++s].to;n.highlight(t.state.doc,l,h,(a,c)=>{let f=t.state.selection.ranges.some(u=>u.from==a&&u.to==c);i.add(a,c,f?hp:lp)})}return i.finish()}},{decorations:n=>n.decorations});function pi(n){return e=>{let t=e.state.field(it,!1);return t&&t.query.spec.valid?n(e,t):ha(e)}}const en=pi((n,{query:e})=>{let{from:t,to:i}=n.state.selection.main,s=e.nextMatch(n.state.doc,t,i);return!s||s.from==t&&s.to==i?!1:(n.dispatch({selection:{anchor:s.from,head:s.to},scrollIntoView:!0,effects:nr(n,s),userEvent:"select.search"}),!0)}),tn=pi((n,{query:e})=>{let{state:t}=n,{from:i,to:s}=t.selection.main,r=e.prevMatch(t.doc,i,s);return r?(n.dispatch({selection:{anchor:r.from,head:r.to},scrollIntoView:!0,effects:nr(n,r),userEvent:"select.search"}),!0):!1}),cp=pi((n,{query:e})=>{let t=e.matchAll(n.state.doc,1e3);return!t||!t.length?!1:(n.dispatch({selection:m.create(t.map(i=>m.range(i.from,i.to))),userEvent:"select.search.matches"}),!0)}),fp=({state:n,dispatch:e})=>{let t=n.selection;if(t.ranges.length>1||t.main.empty)return!1;let{from:i,to:s}=t.main,r=[],o=0;for(let l=new Ht(n.doc,n.sliceDoc(i,s));!l.next().done;){if(r.length>1e3)return!1;l.value.from==i&&(o=r.length),r.push(m.range(l.value.from,l.value.to))}return e(n.update({selection:m.create(r,o),userEvent:"select.search.matches"})),!0},Po=pi((n,{query:e})=>{let{state:t}=n,{from:i,to:s}=t.selection.main;if(t.readOnly)return!1;let r=e.nextMatch(t.doc,i,i);if(!r)return!1;let o=[],l,h,a=[];if(r.from==i&&r.to==s&&(h=t.toText(e.getReplacement(r)),o.push({from:r.from,to:r.to,insert:h}),r=e.nextMatch(t.doc,r.from,r.to),a.push(O.announce.of(t.phrase("replaced match on line $",t.doc.lineAt(i).number)+"."))),r){let c=o.length==0||o[0].from>=r.to?0:r.to-r.from-h.length;l={anchor:r.from-c,head:r.to-c},a.push(nr(n,r))}return n.dispatch({changes:o,selection:l,scrollIntoView:!!l,effects:a,userEvent:"input.replace"}),!0}),up=pi((n,{query:e})=>{if(n.state.readOnly)return!1;let t=e.matchAll(n.state.doc,1e9).map(s=>{let{from:r,to:o}=s;re Line truncated
-1
View File
@@ -1 +0,0 @@
import{S as t,i as n,s as o,K as a}from"./index.66a4e29c.js";function l(e){return a("/collections"),[]}class c extends t{constructor(s){super(),n(this,s,l,null,o,{})}}export{c as default};
@@ -0,0 +1,2 @@
import{S as E,i as G,s as I,F as K,c as F,m as B,t as H,a as N,d as T,C as M,q as J,e as c,w as q,b as k,f as u,r as L,g as b,h as _,u as h,v as O,j as Q,l as U,o as w,A as V,p as W,B as X,D as Y,x as Z,z as S}from"./index.0d48f812.js";function y(f){let e,o,s;return{c(){e=q("for "),o=c("strong"),s=q(f[3]),u(o,"class","txt-nowrap")},m(l,t){b(l,e,t),b(l,o,t),_(o,s)},p(l,t){t&8&&Z(s,l[3])},d(l){l&&w(e),l&&w(o)}}}function x(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password"),l=k(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0,t.autofocus=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[0]),t.focus(),p||(d=h(t,"input",f[6]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&1&&t.value!==n[0]&&S(t,n[0])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function ee(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password confirm"),l=k(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[1]),p||(d=h(t,"input",f[7]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&2&&t.value!==n[1]&&S(t,n[1])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function te(f){let e,o,s,l,t,r,p,d,n,i,g,R,C,v,P,A,j,m=f[3]&&y(f);return r=new J({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),d=new J({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[ee,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),{c(){e=c("form"),o=c("div"),s=c("h4"),l=q(`Reset your admin password
`),m&&m.c(),t=k(),F(r.$$.fragment),p=k(),F(d.$$.fragment),n=k(),i=c("button"),g=c("span"),g.textContent="Set new password",R=k(),C=c("div"),v=c("a"),v.textContent="Back to login",u(s,"class","m-b-xs"),u(o,"class","content txt-center m-b-sm"),u(g,"class","txt"),u(i,"type","submit"),u(i,"class","btn btn-lg btn-block"),i.disabled=f[2],L(i,"btn-loading",f[2]),u(e,"class","m-b-base"),u(v,"href","/login"),u(v,"class","link-hint"),u(C,"class","content txt-center")},m(a,$){b(a,e,$),_(e,o),_(o,s),_(s,l),m&&m.m(s,null),_(e,t),B(r,e,null),_(e,p),B(d,e,null),_(e,n),_(e,i),_(i,g),b(a,R,$),b(a,C,$),_(C,v),P=!0,A||(j=[h(e,"submit",O(f[4])),Q(U.call(null,v))],A=!0)},p(a,$){a[3]?m?m.p(a,$):(m=y(a),m.c(),m.m(s,null)):m&&(m.d(1),m=null);const z={};$&769&&(z.$$scope={dirty:$,ctx:a}),r.$set(z);const D={};$&770&&(D.$$scope={dirty:$,ctx:a}),d.$set(D),(!P||$&4)&&(i.disabled=a[2]),$&4&&L(i,"btn-loading",a[2])},i(a){P||(H(r.$$.fragment,a),H(d.$$.fragment,a),P=!0)},o(a){N(r.$$.fragment,a),N(d.$$.fragment,a),P=!1},d(a){a&&w(e),m&&m.d(),T(r),T(d),a&&w(R),a&&w(C),A=!1,V(j)}}}function se(f){let e,o;return e=new K({props:{$$slots:{default:[te]},$$scope:{ctx:f}}}),{c(){F(e.$$.fragment)},m(s,l){B(e,s,l),o=!0},p(s,[l]){const t={};l&527&&(t.$$scope={dirty:l,ctx:s}),e.$set(t)},i(s){o||(H(e.$$.fragment,s),o=!0)},o(s){N(e.$$.fragment,s),o=!1},d(s){T(e,s)}}}function le(f,e,o){let s,{params:l}=e,t="",r="",p=!1;async function d(){if(!p){o(2,p=!0);try{await W.Admins.confirmPasswordReset(l==null?void 0:l.token,t,r),X("Successfully set a new admin password."),Y("/")}catch(g){W.errorResponseHandler(g)}o(2,p=!1)}}function n(){t=this.value,o(0,t)}function i(){r=this.value,o(1,r)}return f.$$set=g=>{"params"in g&&o(5,l=g.params)},f.$$.update=()=>{f.$$.dirty&32&&o(3,s=M.getJWTPayload(l==null?void 0:l.token).email||"")},[t,r,p,s,d,l,n,i]}class ae extends E{constructor(e){super(),G(this,e,le,se,I,{params:5})}}export{ae as default};
@@ -1,2 +0,0 @@
import{S as I,i as K,s as L,F as W,f as F,m as H,n as B,o as J,q as N,H as M,D as q,e as c,g as u,h as b,j as _,E as O,p as w,z,d as k,A as D,B as T,C as Q,k as U,v as V,I as X,y as E,J as Y,K as Z,G as S}from"./index.66a4e29c.js";function G(f){let e,o,s;return{c(){e=q("for "),o=c("strong"),s=q(f[3]),u(o,"class","txt-nowrap")},m(l,t){b(l,e,t),b(l,o,t),_(o,s)},p(l,t){t&8&&O(s,l[3])},d(l){l&&w(e),l&&w(o)}}}function x(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password"),l=k(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0,t.autofocus=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[0]),t.focus(),p||(d=T(t,"input",f[6]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&1&&t.value!==n[0]&&S(t,n[0])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function ee(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password confirm"),l=k(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[1]),p||(d=T(t,"input",f[7]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&2&&t.value!==n[1]&&S(t,n[1])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function te(f){let e,o,s,l,t,r,p,d,n,i,g,R,C,v,P,A,h,m=f[3]&&G(f);return r=new z({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),d=new z({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[ee,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),{c(){e=c("form"),o=c("div"),s=c("h4"),l=q(`Reset your admin password
`),m&&m.c(),t=k(),F(r.$$.fragment),p=k(),F(d.$$.fragment),n=k(),i=c("button"),g=c("span"),g.textContent="Set new password",R=k(),C=c("div"),v=c("a"),v.textContent="Back to login",u(s,"class","m-b-xs"),u(o,"class","content txt-center m-b-sm"),u(g,"class","txt"),u(i,"type","submit"),u(i,"class","btn btn-lg btn-block"),i.disabled=f[2],D(i,"btn-loading",f[2]),u(e,"class","m-b-base"),u(v,"href","/login"),u(v,"class","link-hint"),u(C,"class","content txt-center")},m(a,$){b(a,e,$),_(e,o),_(o,s),_(s,l),m&&m.m(s,null),_(e,t),H(r,e,null),_(e,p),H(d,e,null),_(e,n),_(e,i),_(i,g),b(a,R,$),b(a,C,$),_(C,v),P=!0,A||(h=[T(e,"submit",Q(f[4])),U(V.call(null,v))],A=!0)},p(a,$){a[3]?m?m.p(a,$):(m=G(a),m.c(),m.m(s,null)):m&&(m.d(1),m=null);const j={};$&769&&(j.$$scope={dirty:$,ctx:a}),r.$set(j);const y={};$&770&&(y.$$scope={dirty:$,ctx:a}),d.$set(y),(!P||$&4)&&(i.disabled=a[2]),$&4&&D(i,"btn-loading",a[2])},i(a){P||(B(r.$$.fragment,a),B(d.$$.fragment,a),P=!0)},o(a){J(r.$$.fragment,a),J(d.$$.fragment,a),P=!1},d(a){a&&w(e),m&&m.d(),N(r),N(d),a&&w(R),a&&w(C),A=!1,X(h)}}}function se(f){let e,o;return e=new W({props:{$$slots:{default:[te]},$$scope:{ctx:f}}}),{c(){F(e.$$.fragment)},m(s,l){H(e,s,l),o=!0},p(s,[l]){const t={};l&527&&(t.$$scope={dirty:l,ctx:s}),e.$set(t)},i(s){o||(B(e.$$.fragment,s),o=!0)},o(s){J(e.$$.fragment,s),o=!1},d(s){N(e,s)}}}function le(f,e,o){let s,{params:l}=e,t="",r="",p=!1;async function d(){if(!p){o(2,p=!0);try{await E.Admins.confirmPasswordReset(l==null?void 0:l.token,t,r),Y("Successfully set a new admin password."),Z("/")}catch(g){E.errorResponseHandler(g)}o(2,p=!1)}}function n(){t=this.value,o(0,t)}function i(){r=this.value,o(1,r)}return f.$$set=g=>{"params"in g&&o(5,l=g.params)},f.$$.update=()=>{f.$$.dirty&32&&o(3,s=M.getJWTPayload(l==null?void 0:l.token).email||"")},[t,r,p,s,d,l,n,i]}class ae extends I{constructor(e){super(),K(this,e,le,se,L,{params:5})}}export{ae as default};
@@ -1,2 +1,2 @@
import{S,i as B,s as M,F as T,f as A,m as E,n as w,o as y,q as H,d as g,e as _,g as p,h as k,j as d,k as j,v as z,w as D,x as G,p as v,y as C,z as N,A as F,B as L,C as I,D as h,E as J,u as P,G as R}from"./index.66a4e29c.js";function K(c){let e,s,n,l,t,o,f,m,i,a,b,u;return l=new N({props:{class:"form-field required",name:"email",$$slots:{default:[Q,({uniqueId:r})=>({5:r}),({uniqueId:r})=>r?32:0]},$$scope:{ctx:c}}}),{c(){e=_("form"),s=_("div"),s.innerHTML=`<h4 class="m-b-xs">Forgotten admin password</h4>
<p>Enter the email associated with your account and we\u2019ll send you a recovery link:</p>`,n=g(),A(l.$$.fragment),t=g(),o=_("button"),f=_("i"),m=g(),i=_("span"),i.textContent="Send recovery link",p(s,"class","content txt-center m-b-sm"),p(f,"class","ri-mail-send-line"),p(i,"class","txt"),p(o,"type","submit"),p(o,"class","btn btn-lg btn-block"),o.disabled=c[1],F(o,"btn-loading",c[1]),p(e,"class","m-b-base")},m(r,$){k(r,e,$),d(e,s),d(e,n),E(l,e,null),d(e,t),d(e,o),d(o,f),d(o,m),d(o,i),a=!0,b||(u=L(e,"submit",I(c[3])),b=!0)},p(r,$){const q={};$&97&&(q.$$scope={dirty:$,ctx:r}),l.$set(q),(!a||$&2)&&(o.disabled=r[1]),$&2&&F(o,"btn-loading",r[1])},i(r){a||(w(l.$$.fragment,r),a=!0)},o(r){y(l.$$.fragment,r),a=!1},d(r){r&&v(e),H(l),b=!1,u()}}}function O(c){let e,s,n,l,t,o,f,m,i;return{c(){e=_("div"),s=_("div"),s.innerHTML='<i class="ri-checkbox-circle-line"></i>',n=g(),l=_("div"),t=_("p"),o=h("Check "),f=_("strong"),m=h(c[0]),i=h(" for the recovery link."),p(s,"class","icon"),p(f,"class","txt-nowrap"),p(l,"class","content"),p(e,"class","alert alert-success")},m(a,b){k(a,e,b),d(e,s),d(e,n),d(e,l),d(l,t),d(t,o),d(t,f),d(f,m),d(t,i)},p(a,b){b&1&&J(m,a[0])},i:P,o:P,d(a){a&&v(e)}}}function Q(c){let e,s,n,l,t,o,f,m;return{c(){e=_("label"),s=h("Email"),l=g(),t=_("input"),p(e,"for",n=c[5]),p(t,"type","email"),p(t,"id",o=c[5]),t.required=!0,t.autofocus=!0},m(i,a){k(i,e,a),d(e,s),k(i,l,a),k(i,t,a),R(t,c[0]),t.focus(),f||(m=L(t,"input",c[4]),f=!0)},p(i,a){a&32&&n!==(n=i[5])&&p(e,"for",n),a&32&&o!==(o=i[5])&&p(t,"id",o),a&1&&t.value!==i[0]&&R(t,i[0])},d(i){i&&v(e),i&&v(l),i&&v(t),f=!1,m()}}}function U(c){let e,s,n,l,t,o,f,m;const i=[O,K],a=[];function b(u,r){return u[2]?0:1}return e=b(c),s=a[e]=i[e](c),{c(){s.c(),n=g(),l=_("div"),t=_("a"),t.textContent="Back to login",p(t,"href","/login"),p(t,"class","link-hint"),p(l,"class","content txt-center")},m(u,r){a[e].m(u,r),k(u,n,r),k(u,l,r),d(l,t),o=!0,f||(m=j(z.call(null,t)),f=!0)},p(u,r){let $=e;e=b(u),e===$?a[e].p(u,r):(D(),y(a[$],1,1,()=>{a[$]=null}),G(),s=a[e],s?s.p(u,r):(s=a[e]=i[e](u),s.c()),w(s,1),s.m(n.parentNode,n))},i(u){o||(w(s),o=!0)},o(u){y(s),o=!1},d(u){a[e].d(u),u&&v(n),u&&v(l),f=!1,m()}}}function V(c){let e,s;return e=new T({props:{$$slots:{default:[U]},$$scope:{ctx:c}}}),{c(){A(e.$$.fragment)},m(n,l){E(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){H(e,n)}}}function W(c,e,s){let n="",l=!1,t=!1;async function o(){if(!l){s(1,l=!0);try{await C.Admins.requestPasswordReset(n),s(2,t=!0)}catch(m){C.errorResponseHandler(m)}s(1,l=!1)}}function f(){n=this.value,s(0,n)}return[n,l,t,o,f]}class Y extends S{constructor(e){super(),B(this,e,W,V,M,{})}}export{Y as default};
import{S as E,i as M,s as T,F as j,c as H,m as L,t as w,a as y,d as S,b as g,e as _,f as p,g as k,h as d,j as z,l as B,k as N,n as D,o as v,p as C,q as G,r as F,u as A,v as I,w as h,x as J,y as P,z as R}from"./index.0d48f812.js";function K(c){let e,s,n,l,t,o,f,m,i,a,b,u;return l=new G({props:{class:"form-field required",name:"email",$$slots:{default:[Q,({uniqueId:r})=>({5:r}),({uniqueId:r})=>r?32:0]},$$scope:{ctx:c}}}),{c(){e=_("form"),s=_("div"),s.innerHTML=`<h4 class="m-b-xs">Forgotten admin password</h4>
<p>Enter the email associated with your account and we\u2019ll send you a recovery link:</p>`,n=g(),H(l.$$.fragment),t=g(),o=_("button"),f=_("i"),m=g(),i=_("span"),i.textContent="Send recovery link",p(s,"class","content txt-center m-b-sm"),p(f,"class","ri-mail-send-line"),p(i,"class","txt"),p(o,"type","submit"),p(o,"class","btn btn-lg btn-block"),o.disabled=c[1],F(o,"btn-loading",c[1]),p(e,"class","m-b-base")},m(r,$){k(r,e,$),d(e,s),d(e,n),L(l,e,null),d(e,t),d(e,o),d(o,f),d(o,m),d(o,i),a=!0,b||(u=A(e,"submit",I(c[3])),b=!0)},p(r,$){const q={};$&97&&(q.$$scope={dirty:$,ctx:r}),l.$set(q),(!a||$&2)&&(o.disabled=r[1]),$&2&&F(o,"btn-loading",r[1])},i(r){a||(w(l.$$.fragment,r),a=!0)},o(r){y(l.$$.fragment,r),a=!1},d(r){r&&v(e),S(l),b=!1,u()}}}function O(c){let e,s,n,l,t,o,f,m,i;return{c(){e=_("div"),s=_("div"),s.innerHTML='<i class="ri-checkbox-circle-line"></i>',n=g(),l=_("div"),t=_("p"),o=h("Check "),f=_("strong"),m=h(c[0]),i=h(" for the recovery link."),p(s,"class","icon"),p(f,"class","txt-nowrap"),p(l,"class","content"),p(e,"class","alert alert-success")},m(a,b){k(a,e,b),d(e,s),d(e,n),d(e,l),d(l,t),d(t,o),d(t,f),d(f,m),d(t,i)},p(a,b){b&1&&J(m,a[0])},i:P,o:P,d(a){a&&v(e)}}}function Q(c){let e,s,n,l,t,o,f,m;return{c(){e=_("label"),s=h("Email"),l=g(),t=_("input"),p(e,"for",n=c[5]),p(t,"type","email"),p(t,"id",o=c[5]),t.required=!0,t.autofocus=!0},m(i,a){k(i,e,a),d(e,s),k(i,l,a),k(i,t,a),R(t,c[0]),t.focus(),f||(m=A(t,"input",c[4]),f=!0)},p(i,a){a&32&&n!==(n=i[5])&&p(e,"for",n),a&32&&o!==(o=i[5])&&p(t,"id",o),a&1&&t.value!==i[0]&&R(t,i[0])},d(i){i&&v(e),i&&v(l),i&&v(t),f=!1,m()}}}function U(c){let e,s,n,l,t,o,f,m;const i=[O,K],a=[];function b(u,r){return u[2]?0:1}return e=b(c),s=a[e]=i[e](c),{c(){s.c(),n=g(),l=_("div"),t=_("a"),t.textContent="Back to login",p(t,"href","/login"),p(t,"class","link-hint"),p(l,"class","content txt-center")},m(u,r){a[e].m(u,r),k(u,n,r),k(u,l,r),d(l,t),o=!0,f||(m=z(B.call(null,t)),f=!0)},p(u,r){let $=e;e=b(u),e===$?a[e].p(u,r):(N(),y(a[$],1,1,()=>{a[$]=null}),D(),s=a[e],s?s.p(u,r):(s=a[e]=i[e](u),s.c()),w(s,1),s.m(n.parentNode,n))},i(u){o||(w(s),o=!0)},o(u){y(s),o=!1},d(u){a[e].d(u),u&&v(n),u&&v(l),f=!1,m()}}}function V(c){let e,s;return e=new j({props:{$$slots:{default:[U]},$$scope:{ctx:c}}}),{c(){H(e.$$.fragment)},m(n,l){L(e,n,l),s=!0},p(n,[l]){const t={};l&71&&(t.$$scope={dirty:l,ctx:n}),e.$set(t)},i(n){s||(w(e.$$.fragment,n),s=!0)},o(n){y(e.$$.fragment,n),s=!1},d(n){S(e,n)}}}function W(c,e,s){let n="",l=!1,t=!1;async function o(){if(!l){s(1,l=!0);try{await C.Admins.requestPasswordReset(n),s(2,t=!0)}catch(m){C.errorResponseHandler(m)}s(1,l=!1)}}function f(){n=this.value,s(0,n)}return[n,l,t,o,f]}class Y extends E{constructor(e){super(),M(this,e,W,V,T,{})}}export{Y as default};
@@ -1,4 +1,4 @@
import{S as z,i as A,s as B,F as D,f as S,m as U,n as $,o as v,q as j,H as G,L as J,h as _,w as M,x as N,p as b,y as P,D as y,e as m,g as d,j as g,E as R,z as W,d as C,A as F,B as E,C as Y,u as h,G as L}from"./index.66a4e29c.js";function I(r){let e,s,t,l,n,o,c,a,i,u,k,q,p=r[3]&&T(r);return o=new W({props:{class:"form-field required",name:"password",$$slots:{default:[O,({uniqueId:f})=>({8:f}),({uniqueId:f})=>f?256:0]},$$scope:{ctx:r}}}),{c(){e=m("form"),s=m("div"),t=m("h4"),l=y(`Type your password to confirm changing your email address
`),p&&p.c(),n=C(),S(o.$$.fragment),c=C(),a=m("button"),i=m("span"),i.textContent="Confirm new email",d(t,"class","m-b-xs"),d(s,"class","content txt-center m-b-sm"),d(i,"class","txt"),d(a,"type","submit"),d(a,"class","btn btn-lg btn-block"),a.disabled=r[1],F(a,"btn-loading",r[1])},m(f,w){_(f,e,w),g(e,s),g(s,t),g(t,l),p&&p.m(t,null),g(e,n),U(o,e,null),g(e,c),g(e,a),g(a,i),u=!0,k||(q=E(e,"submit",Y(r[4])),k=!0)},p(f,w){f[3]?p?p.p(f,w):(p=T(f),p.c(),p.m(t,null)):p&&(p.d(1),p=null);const H={};w&769&&(H.$$scope={dirty:w,ctx:f}),o.$set(H),(!u||w&2)&&(a.disabled=f[1]),w&2&&F(a,"btn-loading",f[1])},i(f){u||($(o.$$.fragment,f),u=!0)},o(f){v(o.$$.fragment,f),u=!1},d(f){f&&b(e),p&&p.d(),j(o),k=!1,q()}}}function K(r){let e,s,t,l,n;return{c(){e=m("div"),e.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
import{S as J,i as M,s as N,F as R,c as S,m as U,t as $,a as v,d as z,C as W,E as Y,g as _,k as j,n as A,o as b,p as F,q as B,e as m,w as y,b as C,f as d,r as H,h as k,u as E,v as D,y as h,x as G,z as T}from"./index.0d48f812.js";function I(r){let e,s,t,l,n,o,c,a,i,u,g,q,p=r[3]&&L(r);return o=new B({props:{class:"form-field required",name:"password",$$slots:{default:[O,({uniqueId:f})=>({8:f}),({uniqueId:f})=>f?256:0]},$$scope:{ctx:r}}}),{c(){e=m("form"),s=m("div"),t=m("h4"),l=y(`Type your password to confirm changing your email address
`),p&&p.c(),n=C(),S(o.$$.fragment),c=C(),a=m("button"),i=m("span"),i.textContent="Confirm new email",d(t,"class","m-b-xs"),d(s,"class","content txt-center m-b-sm"),d(i,"class","txt"),d(a,"type","submit"),d(a,"class","btn btn-lg btn-block"),a.disabled=r[1],H(a,"btn-loading",r[1])},m(f,w){_(f,e,w),k(e,s),k(s,t),k(t,l),p&&p.m(t,null),k(e,n),U(o,e,null),k(e,c),k(e,a),k(a,i),u=!0,g||(q=E(e,"submit",D(r[4])),g=!0)},p(f,w){f[3]?p?p.p(f,w):(p=L(f),p.c(),p.m(t,null)):p&&(p.d(1),p=null);const P={};w&769&&(P.$$scope={dirty:w,ctx:f}),o.$set(P),(!u||w&2)&&(a.disabled=f[1]),w&2&&H(a,"btn-loading",f[1])},i(f){u||($(o.$$.fragment,f),u=!0)},o(f){v(o.$$.fragment,f),u=!1},d(f){f&&b(e),p&&p.d(),z(o),g=!1,q()}}}function K(r){let e,s,t,l,n;return{c(){e=m("div"),e.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
<div class="content txt-bold"><p>Email address changed</p>
<p>You can now sign in with your new email address.</p></div>`,s=C(),t=m("button"),t.textContent="Close",d(e,"class","alert alert-success"),d(t,"type","button"),d(t,"class","btn btn-secondary btn-block")},m(o,c){_(o,e,c),_(o,s,c),_(o,t,c),l||(n=E(t,"click",r[6]),l=!0)},p:h,i:h,o:h,d(o){o&&b(e),o&&b(s),o&&b(t),l=!1,n()}}}function T(r){let e,s,t;return{c(){e=y("to "),s=m("strong"),t=y(r[3]),d(s,"class","txt-nowrap")},m(l,n){_(l,e,n),_(l,s,n),g(s,t)},p(l,n){n&8&&R(t,l[3])},d(l){l&&b(e),l&&b(s)}}}function O(r){let e,s,t,l,n,o,c,a;return{c(){e=m("label"),s=y("Password"),l=C(),n=m("input"),d(e,"for",t=r[8]),d(n,"type","password"),d(n,"id",o=r[8]),n.required=!0,n.autofocus=!0},m(i,u){_(i,e,u),g(e,s),_(i,l,u),_(i,n,u),L(n,r[0]),n.focus(),c||(a=E(n,"input",r[7]),c=!0)},p(i,u){u&256&&t!==(t=i[8])&&d(e,"for",t),u&256&&o!==(o=i[8])&&d(n,"id",o),u&1&&n.value!==i[0]&&L(n,i[0])},d(i){i&&b(e),i&&b(l),i&&b(n),c=!1,a()}}}function Q(r){let e,s,t,l;const n=[K,I],o=[];function c(a,i){return a[2]?0:1}return e=c(r),s=o[e]=n[e](r),{c(){s.c(),t=J()},m(a,i){o[e].m(a,i),_(a,t,i),l=!0},p(a,i){let u=e;e=c(a),e===u?o[e].p(a,i):(M(),v(o[u],1,1,()=>{o[u]=null}),N(),s=o[e],s?s.p(a,i):(s=o[e]=n[e](a),s.c()),$(s,1),s.m(t.parentNode,t))},i(a){l||($(s),l=!0)},o(a){v(s),l=!1},d(a){o[e].d(a),a&&b(t)}}}function V(r){let e,s;return e=new D({props:{nobranding:!0,$$slots:{default:[Q]},$$scope:{ctx:r}}}),{c(){S(e.$$.fragment)},m(t,l){U(e,t,l),s=!0},p(t,[l]){const n={};l&527&&(n.$$scope={dirty:l,ctx:t}),e.$set(n)},i(t){s||($(e.$$.fragment,t),s=!0)},o(t){v(e.$$.fragment,t),s=!1},d(t){j(e,t)}}}function X(r,e,s){let t,{params:l}=e,n="",o=!1,c=!1;async function a(){if(!o){s(1,o=!0);try{await P.Users.confirmEmailChange(l==null?void 0:l.token,n),s(2,c=!0)}catch(k){P.errorResponseHandler(k)}s(1,o=!1)}}const i=()=>window.close();function u(){n=this.value,s(0,n)}return r.$$set=k=>{"params"in k&&s(5,l=k.params)},r.$$.update=()=>{r.$$.dirty&32&&s(3,t=G.getJWTPayload(l==null?void 0:l.token).newEmail||"")},[n,o,c,t,a,l,i,u]}class x extends z{constructor(e){super(),A(this,e,X,V,B,{params:5})}}export{x as default};
<p>You can now sign in with your new email address.</p></div>`,s=C(),t=m("button"),t.textContent="Close",d(e,"class","alert alert-success"),d(t,"type","button"),d(t,"class","btn btn-secondary btn-block")},m(o,c){_(o,e,c),_(o,s,c),_(o,t,c),l||(n=E(t,"click",r[6]),l=!0)},p:h,i:h,o:h,d(o){o&&b(e),o&&b(s),o&&b(t),l=!1,n()}}}function L(r){let e,s,t;return{c(){e=y("to "),s=m("strong"),t=y(r[3]),d(s,"class","txt-nowrap")},m(l,n){_(l,e,n),_(l,s,n),k(s,t)},p(l,n){n&8&&G(t,l[3])},d(l){l&&b(e),l&&b(s)}}}function O(r){let e,s,t,l,n,o,c,a;return{c(){e=m("label"),s=y("Password"),l=C(),n=m("input"),d(e,"for",t=r[8]),d(n,"type","password"),d(n,"id",o=r[8]),n.required=!0,n.autofocus=!0},m(i,u){_(i,e,u),k(e,s),_(i,l,u),_(i,n,u),T(n,r[0]),n.focus(),c||(a=E(n,"input",r[7]),c=!0)},p(i,u){u&256&&t!==(t=i[8])&&d(e,"for",t),u&256&&o!==(o=i[8])&&d(n,"id",o),u&1&&n.value!==i[0]&&T(n,i[0])},d(i){i&&b(e),i&&b(l),i&&b(n),c=!1,a()}}}function Q(r){let e,s,t,l;const n=[K,I],o=[];function c(a,i){return a[2]?0:1}return e=c(r),s=o[e]=n[e](r),{c(){s.c(),t=Y()},m(a,i){o[e].m(a,i),_(a,t,i),l=!0},p(a,i){let u=e;e=c(a),e===u?o[e].p(a,i):(j(),v(o[u],1,1,()=>{o[u]=null}),A(),s=o[e],s?s.p(a,i):(s=o[e]=n[e](a),s.c()),$(s,1),s.m(t.parentNode,t))},i(a){l||($(s),l=!0)},o(a){v(s),l=!1},d(a){o[e].d(a),a&&b(t)}}}function V(r){let e,s;return e=new R({props:{nobranding:!0,$$slots:{default:[Q]},$$scope:{ctx:r}}}),{c(){S(e.$$.fragment)},m(t,l){U(e,t,l),s=!0},p(t,[l]){const n={};l&527&&(n.$$scope={dirty:l,ctx:t}),e.$set(n)},i(t){s||($(e.$$.fragment,t),s=!0)},o(t){v(e.$$.fragment,t),s=!1},d(t){z(e,t)}}}function X(r,e,s){let t,{params:l}=e,n="",o=!1,c=!1;async function a(){if(!o){s(1,o=!0);try{await F.Users.confirmEmailChange(l==null?void 0:l.token,n),s(2,c=!0)}catch(g){F.errorResponseHandler(g)}s(1,o=!1)}}const i=()=>window.close();function u(){n=this.value,s(0,n)}return r.$$set=g=>{"params"in g&&s(5,l=g.params)},r.$$.update=()=>{r.$$.dirty&32&&s(3,t=W.getJWTPayload(l==null?void 0:l.token).newEmail||"")},[n,o,c,t,a,l,i,u]}class x extends J{constructor(e){super(),M(this,e,X,V,N,{params:5})}}export{x as default};
@@ -0,0 +1,4 @@
import{S as W,i as Y,s as j,F as A,c as H,m as N,t as P,a as q,d as S,C as B,E as D,g as _,k as G,n as I,o as m,p as z,q as E,e as b,w as y,b as C,f as c,r as J,h as w,u as h,v as K,y as F,x as O,z as R}from"./index.0d48f812.js";function Q(i){let e,l,t,n,s,o,p,a,r,u,v,g,k,L,d=i[4]&&M(i);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:i}}}),a=new E({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[Z,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:i}}}),{c(){e=b("form"),l=b("div"),t=b("h4"),n=y(`Reset your user password
`),d&&d.c(),s=C(),H(o.$$.fragment),p=C(),H(a.$$.fragment),r=C(),u=b("button"),v=b("span"),v.textContent="Set new password",c(t,"class","m-b-xs"),c(l,"class","content txt-center m-b-sm"),c(v,"class","txt"),c(u,"type","submit"),c(u,"class","btn btn-lg btn-block"),u.disabled=i[2],J(u,"btn-loading",i[2])},m(f,$){_(f,e,$),w(e,l),w(l,t),w(t,n),d&&d.m(t,null),w(e,s),N(o,e,null),w(e,p),N(a,e,null),w(e,r),w(e,u),w(u,v),g=!0,k||(L=h(e,"submit",K(i[5])),k=!0)},p(f,$){f[4]?d?d.p(f,$):(d=M(f),d.c(),d.m(t,null)):d&&(d.d(1),d=null);const T={};$&3073&&(T.$$scope={dirty:$,ctx:f}),o.$set(T);const U={};$&3074&&(U.$$scope={dirty:$,ctx:f}),a.$set(U),(!g||$&4)&&(u.disabled=f[2]),$&4&&J(u,"btn-loading",f[2])},i(f){g||(P(o.$$.fragment,f),P(a.$$.fragment,f),g=!0)},o(f){q(o.$$.fragment,f),q(a.$$.fragment,f),g=!1},d(f){f&&m(e),d&&d.d(),S(o),S(a),k=!1,L()}}}function V(i){let e,l,t,n,s;return{c(){e=b("div"),e.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
<div class="content txt-bold"><p>Password changed</p>
<p>You can now sign in with your new password.</p></div>`,l=C(),t=b("button"),t.textContent="Close",c(e,"class","alert alert-success"),c(t,"type","button"),c(t,"class","btn btn-secondary btn-block")},m(o,p){_(o,e,p),_(o,l,p),_(o,t,p),n||(s=h(t,"click",i[7]),n=!0)},p:F,i:F,o:F,d(o){o&&m(e),o&&m(l),o&&m(t),n=!1,s()}}}function M(i){let e,l,t;return{c(){e=y("for "),l=b("strong"),t=y(i[4])},m(n,s){_(n,e,s),_(n,l,s),w(l,t)},p(n,s){s&16&&O(t,n[4])},d(n){n&&m(e),n&&m(l)}}}function X(i){let e,l,t,n,s,o,p,a;return{c(){e=b("label"),l=y("New password"),n=C(),s=b("input"),c(e,"for",t=i[10]),c(s,"type","password"),c(s,"id",o=i[10]),s.required=!0,s.autofocus=!0},m(r,u){_(r,e,u),w(e,l),_(r,n,u),_(r,s,u),R(s,i[0]),s.focus(),p||(a=h(s,"input",i[8]),p=!0)},p(r,u){u&1024&&t!==(t=r[10])&&c(e,"for",t),u&1024&&o!==(o=r[10])&&c(s,"id",o),u&1&&s.value!==r[0]&&R(s,r[0])},d(r){r&&m(e),r&&m(n),r&&m(s),p=!1,a()}}}function Z(i){let e,l,t,n,s,o,p,a;return{c(){e=b("label"),l=y("New password confirm"),n=C(),s=b("input"),c(e,"for",t=i[10]),c(s,"type","password"),c(s,"id",o=i[10]),s.required=!0},m(r,u){_(r,e,u),w(e,l),_(r,n,u),_(r,s,u),R(s,i[1]),p||(a=h(s,"input",i[9]),p=!0)},p(r,u){u&1024&&t!==(t=r[10])&&c(e,"for",t),u&1024&&o!==(o=r[10])&&c(s,"id",o),u&2&&s.value!==r[1]&&R(s,r[1])},d(r){r&&m(e),r&&m(n),r&&m(s),p=!1,a()}}}function x(i){let e,l,t,n;const s=[V,Q],o=[];function p(a,r){return a[3]?0:1}return e=p(i),l=o[e]=s[e](i),{c(){l.c(),t=D()},m(a,r){o[e].m(a,r),_(a,t,r),n=!0},p(a,r){let u=e;e=p(a),e===u?o[e].p(a,r):(G(),q(o[u],1,1,()=>{o[u]=null}),I(),l=o[e],l?l.p(a,r):(l=o[e]=s[e](a),l.c()),P(l,1),l.m(t.parentNode,t))},i(a){n||(P(l),n=!0)},o(a){q(l),n=!1},d(a){o[e].d(a),a&&m(t)}}}function ee(i){let e,l;return e=new A({props:{nobranding:!0,$$slots:{default:[x]},$$scope:{ctx:i}}}),{c(){H(e.$$.fragment)},m(t,n){N(e,t,n),l=!0},p(t,[n]){const s={};n&2079&&(s.$$scope={dirty:n,ctx:t}),e.$set(s)},i(t){l||(P(e.$$.fragment,t),l=!0)},o(t){q(e.$$.fragment,t),l=!1},d(t){S(e,t)}}}function te(i,e,l){let t,{params:n}=e,s="",o="",p=!1,a=!1;async function r(){if(!p){l(2,p=!0);try{await z.Users.confirmPasswordReset(n==null?void 0:n.token,s,o),l(3,a=!0)}catch(k){z.errorResponseHandler(k)}l(2,p=!1)}}const u=()=>window.close();function v(){s=this.value,l(0,s)}function g(){o=this.value,l(1,o)}return i.$$set=k=>{"params"in k&&l(6,n=k.params)},i.$$.update=()=>{i.$$.dirty&64&&l(4,t=B.getJWTPayload(n==null?void 0:n.token).email||"")},[s,o,p,a,t,r,n,u,v,g]}class le extends W{constructor(e){super(),Y(this,e,te,ee,j,{params:6})}}export{le as default};
@@ -1,4 +0,0 @@
import{S as D,i as E,s as G,F as J,f as F,m as L,n as P,o as q,q as N,H as M,L as W,h as _,w as Y,x as I,p as m,y as j,D as y,e as b,j as w,E as K,z,d as C,g as c,A,B as R,C as O,u as h,G as H}from"./index.66a4e29c.js";function Q(r){let e,l,t,n,s,o,p,a,i,u,v,g,k,S,d=r[4]&&B(r);return o=new z({props:{class:"form-field required",name:"password",$$slots:{default:[X,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:r}}}),a=new z({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[Z,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:r}}}),{c(){e=b("form"),l=b("div"),t=b("h4"),n=y(`Reset your user password
`),d&&d.c(),s=C(),F(o.$$.fragment),p=C(),F(a.$$.fragment),i=C(),u=b("button"),v=b("span"),v.textContent="Set new password",c(t,"class","m-b-xs"),c(l,"class","content txt-center m-b-sm"),c(v,"class","txt"),c(u,"type","submit"),c(u,"class","btn btn-lg btn-block"),u.disabled=r[2],A(u,"btn-loading",r[2])},m(f,$){_(f,e,$),w(e,l),w(l,t),w(t,n),d&&d.m(t,null),w(e,s),L(o,e,null),w(e,p),L(a,e,null),w(e,i),w(e,u),w(u,v),g=!0,k||(S=R(e,"submit",O(r[5])),k=!0)},p(f,$){f[4]?d?d.p(f,$):(d=B(f),d.c(),d.m(t,null)):d&&(d.d(1),d=null);const T={};$&3073&&(T.$$scope={dirty:$,ctx:f}),o.$set(T);const U={};$&3074&&(U.$$scope={dirty:$,ctx:f}),a.$set(U),(!g||$&4)&&(u.disabled=f[2]),$&4&&A(u,"btn-loading",f[2])},i(f){g||(P(o.$$.fragment,f),P(a.$$.fragment,f),g=!0)},o(f){q(o.$$.fragment,f),q(a.$$.fragment,f),g=!1},d(f){f&&m(e),d&&d.d(),N(o),N(a),k=!1,S()}}}function V(r){let e,l,t,n,s;return{c(){e=b("div"),e.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
<div class="content txt-bold"><p>Password changed</p>
<p>You can now sign in with your new password.</p></div>`,l=C(),t=b("button"),t.textContent="Close",c(e,"class","alert alert-success"),c(t,"type","button"),c(t,"class","btn btn-secondary btn-block")},m(o,p){_(o,e,p),_(o,l,p),_(o,t,p),n||(s=R(t,"click",r[7]),n=!0)},p:h,i:h,o:h,d(o){o&&m(e),o&&m(l),o&&m(t),n=!1,s()}}}function B(r){let e,l,t;return{c(){e=y("for "),l=b("strong"),t=y(r[4])},m(n,s){_(n,e,s),_(n,l,s),w(l,t)},p(n,s){s&16&&K(t,n[4])},d(n){n&&m(e),n&&m(l)}}}function X(r){let e,l,t,n,s,o,p,a;return{c(){e=b("label"),l=y("New password"),n=C(),s=b("input"),c(e,"for",t=r[10]),c(s,"type","password"),c(s,"id",o=r[10]),s.required=!0,s.autofocus=!0},m(i,u){_(i,e,u),w(e,l),_(i,n,u),_(i,s,u),H(s,r[0]),s.focus(),p||(a=R(s,"input",r[8]),p=!0)},p(i,u){u&1024&&t!==(t=i[10])&&c(e,"for",t),u&1024&&o!==(o=i[10])&&c(s,"id",o),u&1&&s.value!==i[0]&&H(s,i[0])},d(i){i&&m(e),i&&m(n),i&&m(s),p=!1,a()}}}function Z(r){let e,l,t,n,s,o,p,a;return{c(){e=b("label"),l=y("New password confirm"),n=C(),s=b("input"),c(e,"for",t=r[10]),c(s,"type","password"),c(s,"id",o=r[10]),s.required=!0},m(i,u){_(i,e,u),w(e,l),_(i,n,u),_(i,s,u),H(s,r[1]),p||(a=R(s,"input",r[9]),p=!0)},p(i,u){u&1024&&t!==(t=i[10])&&c(e,"for",t),u&1024&&o!==(o=i[10])&&c(s,"id",o),u&2&&s.value!==i[1]&&H(s,i[1])},d(i){i&&m(e),i&&m(n),i&&m(s),p=!1,a()}}}function x(r){let e,l,t,n;const s=[V,Q],o=[];function p(a,i){return a[3]?0:1}return e=p(r),l=o[e]=s[e](r),{c(){l.c(),t=W()},m(a,i){o[e].m(a,i),_(a,t,i),n=!0},p(a,i){let u=e;e=p(a),e===u?o[e].p(a,i):(Y(),q(o[u],1,1,()=>{o[u]=null}),I(),l=o[e],l?l.p(a,i):(l=o[e]=s[e](a),l.c()),P(l,1),l.m(t.parentNode,t))},i(a){n||(P(l),n=!0)},o(a){q(l),n=!1},d(a){o[e].d(a),a&&m(t)}}}function ee(r){let e,l;return e=new J({props:{nobranding:!0,$$slots:{default:[x]},$$scope:{ctx:r}}}),{c(){F(e.$$.fragment)},m(t,n){L(e,t,n),l=!0},p(t,[n]){const s={};n&2079&&(s.$$scope={dirty:n,ctx:t}),e.$set(s)},i(t){l||(P(e.$$.fragment,t),l=!0)},o(t){q(e.$$.fragment,t),l=!1},d(t){N(e,t)}}}function te(r,e,l){let t,{params:n}=e,s="",o="",p=!1,a=!1;async function i(){if(!p){l(2,p=!0);try{await j.Users.confirmPasswordReset(n==null?void 0:n.token,s,o),l(3,a=!0)}catch(k){j.errorResponseHandler(k)}l(2,p=!1)}}const u=()=>window.close();function v(){s=this.value,l(0,s)}function g(){o=this.value,l(1,o)}return r.$$set=k=>{"params"in k&&l(6,n=k.params)},r.$$.update=()=>{r.$$.dirty&64&&l(4,t=M.getJWTPayload(n==null?void 0:n.token).email||"")},[s,o,p,a,t,i,n,u,v,g]}class le extends D{constructor(e){super(),E(this,e,te,ee,G,{params:6})}}export{le as default};
@@ -1,3 +1,3 @@
import{S as k,i as v,s as y,F as w,f as x,m as C,n as g,o as L,q as $,y as H,L as M,h as r,p as a,e as u,d as m,g as f,B as _,u as p}from"./index.66a4e29c.js";function P(o){let t,s,e,n,i;return{c(){t=u("div"),t.innerHTML=`<div class="icon"><i class="ri-error-warning-line"></i></div>
import{S as k,i as v,s as y,F as w,c as x,m as C,t as g,a as $,d as L,p as H,E as M,g as r,o as a,e as u,b as m,f,u as _,y as p}from"./index.0d48f812.js";function P(o){let t,s,e,n,i;return{c(){t=u("div"),t.innerHTML=`<div class="icon"><i class="ri-error-warning-line"></i></div>
<div class="content txt-bold"><p>Invalid or expired verification token.</p></div>`,s=m(),e=u("button"),e.textContent="Close",f(t,"class","alert alert-danger"),f(e,"type","button"),f(e,"class","btn btn-secondary btn-block")},m(l,c){r(l,t,c),r(l,s,c),r(l,e,c),n||(i=_(e,"click",o[4]),n=!0)},p,d(l){l&&a(t),l&&a(s),l&&a(e),n=!1,i()}}}function S(o){let t,s,e,n,i;return{c(){t=u("div"),t.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
<div class="content txt-bold"><p>Successfully verified email address.</p></div>`,s=m(),e=u("button"),e.textContent="Close",f(t,"class","alert alert-success"),f(e,"type","button"),f(e,"class","btn btn-secondary btn-block")},m(l,c){r(l,t,c),r(l,s,c),r(l,e,c),n||(i=_(e,"click",o[3]),n=!0)},p,d(l){l&&a(t),l&&a(s),l&&a(e),n=!1,i()}}}function T(o){let t;return{c(){t=u("div"),t.innerHTML='<div class="loader loader-lg"><em>Please wait...</em></div>',f(t,"class","txt-center")},m(s,e){r(s,t,e)},p,d(s){s&&a(t)}}}function q(o){let t;function s(i,l){return i[1]?T:i[0]?S:P}let e=s(o),n=e(o);return{c(){n.c(),t=M()},m(i,l){n.m(i,l),r(i,t,l)},p(i,l){e===(e=s(i))&&n?n.p(i,l):(n.d(1),n=e(i),n&&(n.c(),n.m(t.parentNode,t)))},d(i){n.d(i),i&&a(t)}}}function F(o){let t,s;return t=new w({props:{nobranding:!0,$$slots:{default:[q]},$$scope:{ctx:o}}}),{c(){x(t.$$.fragment)},m(e,n){C(t,e,n),s=!0},p(e,[n]){const i={};n&67&&(i.$$scope={dirty:n,ctx:e}),t.$set(i)},i(e){s||(g(t.$$.fragment,e),s=!0)},o(e){L(t.$$.fragment,e),s=!1},d(e){$(t,e)}}}function U(o,t,s){let{params:e}=t,n=!1,i=!1;l();async function l(){s(1,i=!0);try{await H.Users.confirmVerification(e==null?void 0:e.token),s(0,n=!0)}catch(d){console.warn(d),s(0,n=!1)}s(1,i=!1)}const c=()=>window.close(),b=()=>window.close();return o.$$set=d=>{"params"in d&&s(2,e=d.params)},[n,i,e,c,b]}class B extends k{constructor(t){super(),v(this,t,U,F,y,{params:2})}}export{B as default};
<div class="content txt-bold"><p>Successfully verified email address.</p></div>`,s=m(),e=u("button"),e.textContent="Close",f(t,"class","alert alert-success"),f(e,"type","button"),f(e,"class","btn btn-secondary btn-block")},m(l,c){r(l,t,c),r(l,s,c),r(l,e,c),n||(i=_(e,"click",o[3]),n=!0)},p,d(l){l&&a(t),l&&a(s),l&&a(e),n=!1,i()}}}function T(o){let t;return{c(){t=u("div"),t.innerHTML='<div class="loader loader-lg"><em>Please wait...</em></div>',f(t,"class","txt-center")},m(s,e){r(s,t,e)},p,d(s){s&&a(t)}}}function F(o){let t;function s(i,l){return i[1]?T:i[0]?S:P}let e=s(o),n=e(o);return{c(){n.c(),t=M()},m(i,l){n.m(i,l),r(i,t,l)},p(i,l){e===(e=s(i))&&n?n.p(i,l):(n.d(1),n=e(i),n&&(n.c(),n.m(t.parentNode,t)))},d(i){n.d(i),i&&a(t)}}}function U(o){let t,s;return t=new w({props:{nobranding:!0,$$slots:{default:[F]},$$scope:{ctx:o}}}),{c(){x(t.$$.fragment)},m(e,n){C(t,e,n),s=!0},p(e,[n]){const i={};n&67&&(i.$$scope={dirty:n,ctx:e}),t.$set(i)},i(e){s||(g(t.$$.fragment,e),s=!0)},o(e){$(t.$$.fragment,e),s=!1},d(e){L(t,e)}}}function V(o,t,s){let{params:e}=t,n=!1,i=!1;l();async function l(){s(1,i=!0);try{await H.Users.confirmVerification(e==null?void 0:e.token),s(0,n=!0)}catch(d){console.warn(d),s(0,n=!1)}s(1,i=!1)}const c=()=>window.close(),b=()=>window.close();return o.$$set=d=>{"params"in d&&s(2,e=d.params)},[n,i,e,c,b]}class E extends k{constructor(t){super(),v(this,t,V,U,y,{params:2})}}export{E as default};
Loaded 100 of 158 files, more files were not shown because too many files have changed in this diff. Show more