Compare commits

...
41 Commits
Author SHA1 Message Date
Gani Georgiev 5684ee24f1 updated changelog 2026-09-06 20:14:05 +03:00
Gani Georgiev b3de909790 [#7834] changed definition to interface 2026-09-06 20:11:04 +03:00
Gani Georgiev a53f189f74 bumped app version 2026-09-06 19:54:38 +03:00
Gani Georgiev 7ff7e0f043 handled io.ReadAll error with exact payload size 2026-09-06 19:51:06 +03:00
Gani Georgiev 6e29284e6a updated changelogs 2026-09-06 17:22:45 +03:00
Gani Georgiev 1984952e4e fixed multiline index column and utrigger index validations if failing to parse all columns 2026-09-06 17:18:49 +03:00
Gani Georgiev f611256d90 removed manual unescape as jsonv2 does it alreadyfor us 2026-09-06 16:29:52 +03:00
Gani Georgiev 10f4f28312 updated test 2026-09-06 15:37:16 +03:00
Gani Georgiev 44f817e7f1 fixed nested self-referenced cascade delete 2026-09-06 15:35:16 +03:00
Gani Georgiev 75c6a4fd1f updated json field validator and allow duplicated keys when serializing record models 2026-09-06 12:31:52 +03:00
Gani Georgiev 57c0b034e6 fixed non-ascii dash in hinted proxy header 2026-09-06 11:22:01 +03:00
Gani Georgiev 4709f63183 optional assert in case the provider doesn't return id_token as string 2026-09-06 11:18:03 +03:00
Gani Georgiev 53a6cd04e2 refactored body limit middleware to prevent unnecessery reads if already beyound the limit 2026-09-06 10:47:59 +03:00
Gani Georgiev df4e6eeb35 clamped arccosine to [-1,1] to prevent rounding errors in geoDistance 2026-09-06 00:45:12 +03:00
Gani Georgiev dbea21f1b2 fixed index validator to allow expressions in the optional where clause 2026-09-05 22:31:16 +03:00
Gani Georgiev 97f9d63a1b commit json status write after checking the fields picker 2026-09-05 18:32:30 +03:00
Gani Georgiev bf12ae0244 updated dark primary btn colors 2026-09-04 00:12:32 +03:00
Gani Georgiev 7ac159b61b bumped golang.org/x/* deps and updated dark mode primary btn contrast 2026-09-03 23:41:10 +03:00
Gani Georgiev 8213ccffb4 renamed variable to avoid confusion 2026-09-02 15:59:58 +03:00
Gani Georgiev e16a9f8b21 bumped goja and regenerated jsvm types 2026-09-02 15:44:19 +03:00
Gani Georgiev 0206f6135c bumped app version 2026-09-02 15:39:09 +03:00
Gani Georgiev e417a26a05 updated backport changelog 2026-09-02 12:41:04 +03:00
Gani Georgiev ac4c30ccbc updated json error test 2026-09-02 12:40:29 +03:00
Gani Georgiev fed5ab9a13 fixed linter warnings 2026-09-02 12:25:01 +03:00
Gani Georgiev 6d9ca9bf9d bumped min go github version to 1.27.1 2026-09-02 12:18:12 +03:00
Gani Georgiev 56f1d1dfdd return an error on invalid fallback param serialization 2026-09-02 12:15:34 +03:00
Gani Georgiev 1b3edbbf5c added invalid index name test 2026-09-02 10:57:00 +03:00
Gani Georgiev 522f9e0ab0 fixed index parsing error when no index name is provided 2026-09-02 10:48:24 +03:00
Gani Georgiev 3c9427667d autocomplete by the word up to the the caret position 2026-09-02 09:37:27 +03:00
Gani Georgiev fd5b9e5f75 clear autocomplete dropdown if empty 2026-09-02 08:10:49 +03:00
Gani Georgiev d0b441bc24 don't show autocomplete if the only suggestion is exact match 2026-09-02 08:05:30 +03:00
Gani Georgiev 229240b374 updated changelog and bumped dev app version 2026-09-02 06:45:06 +03:00
Gani Georgiev f694893d31 bumped highlight limit and slightly debounce autocomplete suggestions 2026-09-02 06:43:11 +03:00
Gani Georgiev 8cb486f24a optimized filter params replacement to use strings.Replacer for a single pass 2026-08-28 12:28:16 +03:00
Gani Georgiev bc8ffed4e7 [#7815] added checks for missing providers prop 2026-08-24 18:00:11 +03:00
Gani Georgiev da18302785 bumped app version 2026-08-24 17:27:07 +03:00
Gani Georgiev b402f11a79 enabled invalid utf8 character replacement by default for the custom json types 2026-08-24 17:26:36 +03:00
Gani Georgiev 9f4f4d4e73 fixed typos 2026-08-24 17:26:01 +03:00
Gani Georgiev d763d3dff1 [#7815] fixed oauth2 providers config merge 2026-08-24 17:07:51 +03:00
Gani Georgiev 04ed202c78 removed unnecessery empty array normalizations because encoding/json/v2 does it by default 2026-08-24 16:46:59 +03:00
Gani Georgiev 756b326bfa allow invalid utf8 characters when marshalizing json responses 2026-08-24 14:13:53 +03:00
52 changed files with 4379 additions and 3532 deletions

No files matched your search

+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: '>=1.27.0'
go-version: '>=1.27.1'
# This step usually is not needed because the /ui/dist is pregenerated locally
# but its here to ensure that each release embeds the latest admin ui artifacts.
+44
View File
@@ -1,3 +1,47 @@
## v0.40.3
- Write the status header for JSON responses only if the fields picker succeed or has acceptable fallback.
_This is to allow custom response status code for failed json writes._
- Fixed collection index validator to allow expressions with parenthesis in the optional `WHERE` clause.
- Clamped arccosine to [-1,1] in the Harvesine formula for the `geoDistance()` filter function to workaround edge case related to float rounding errors for some coordinates.
- Prevent unnecessary body chunk read if we already known that we are beyond the allowed limit.
- Updated the `json` field validator to check the `encoding/json/v2` semantics and allow duplicated keys on record marshalize for compliance with old jsonv1 data.
- Fixed nested cascade delete of self-referenced relation records.
- Minor UI fixes (updated dark primary btn color contrast, force reload the records list if the deleted record has self-referenced cascade relation field, etc.).
- Changed JSVM `$app` variable definition from TS type to interface ([#7834](https://github.com/pocketbase/pocketbase/issues/7834)).
- Bumped `golang.org/x/*` dependencies to silence security scanners ([#7829](https://github.com/pocketbase/pocketbase/discussions/7829)).
## v0.40.2
- Return an error when filter params fallback fails to json serialize and optimized params replacement to execute in a single pass.
- Fixed collection index parsing error for indexes with missing name.
- Minor UI autocomplete optimizations _(prefix match, autocomplete debounce, etc.)_.
- Fixed linter warnings and comment typos.
- Bumped goja and its related dependencies _(regex unescaped dash error fix and base64 optimizations)_.
- Bumped the min Go GitHub action version to 1.27.1 as it includes some [minor `database/sql` and `enconding/json/v2` bug fixes](https://github.com/golang/go/issues?q=milestone%3AGo1.27.1).
## v0.40.1
- Fixes for some reported regressions related to the `encoding/json/v2` update:
- allow mangling invalid UTF8 characters when serializing json data ([#7814](https://github.com/pocketbase/pocketbase/issues/7814))
- fixed OAuth2 providers config merge incorrectly replacing the entire slice ([#7815](https://github.com/pocketbase/pocketbase/issues/7815))
## v0.40.0
- Propagate console command errors and recovered panics to `app.Start()` so that the program can exit with non-zero code while still ensuring that `app.OnTerminate` hook was triggered _(responsible for the app graceful shutdown handling)_.
+18
View File
@@ -2,6 +2,24 @@
> For the most recent versions, please refer to [CHANGELOG.md](./CHANGELOG.md)
---
## v0.22.55
- (_Backported from v0.40.3_) Fixed collection index validator to allow expressions with parenthesis in the optional `WHERE` clause.
- (_Backported from v0.40.3_) Fixed nested cascade delete of self-referenced relation records.
- (_Backported from v0.40.3_) Bumped `golang.org/x/*` dependencies to silence security scanners ([#7829](https://github.com/pocketbase/pocketbase/discussions/7829)).
## v0.22.54
- (_Backported from v0.40.2_) Bumped goja and its related dependencies _(regex unescaped dash error fix and base64 optimizations)_.
- (_Backported from v0.40.2_) Return an error when filter params fallback fails to json serialize and optimized params replacement to execute in a single pass.
- (_Backported from v0.40.2_) Bumped the min Go GitHub action version to 1.27.1 as it includes some [minor `database/sql` and `enconding/json/v2` bug fixes](https://github.com/golang/go/issues?q=milestone%3AGo1.27.1).
## v0.22.53
- Workaround `encoding/json` infinite recursion when used with Go 1.27 ([#7808](https://github.com/pocketbase/pocketbase/issues/7808)).
+65
View File
@@ -1225,6 +1225,71 @@ func TestCollectionUpdate(t *testing.T) {
"OnModelValidate": 1,
},
},
{
Name: "add another OAuth2 provider to an auth collection",
Method: http.MethodPatch,
URL: "/api/collections/users",
Body: strings.NewReader(`{
"oauth2": {
"providers": [
{"name": "apple", "clientId": "a", "clientSecret": "b"},
{
"pkce": null,
"name": "google",
"authURL": "",
"displayName": "existing",
"extra": {}
}
]
}
}`),
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
// verify that the collection has google and gitlab as OAuth2 providers
users, err := app.FindCollectionByNameOrId("users")
if err != nil {
t.Fatal(err)
}
if v := len(users.OAuth2.Providers); v != 2 {
t.Fatalf("Expected 2 OAuth2 providers, got %d", v)
}
if v := users.OAuth2.Providers[0].Name; v != "gitlab" {
t.Fatalf("Expected provider 0 to be %s, got %s", "gitlab", v)
}
if v := users.OAuth2.Providers[1].Name; v != "google" {
t.Fatalf("Expected provider 1 to be %s, got %s", "google", v)
}
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"name":"google"`,
`"name":"apple"`,
`"displayName":"existing"`,
`"clientId":"test"`,
`"clientId":"a"`,
},
NotExpectedContent: []string{
`"name":"gitlab"`,
`clientSecret`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnCollectionUpdateRequest": 1,
"OnCollectionUpdate": 1,
"OnCollectionUpdateExecute": 1,
"OnCollectionAfterUpdateSuccess": 1,
"OnCollectionValidate": 1,
"OnModelUpdate": 1,
"OnModelUpdateExecute": 1,
"OnModelAfterUpdateSuccess": 1,
"OnModelValidate": 1,
},
},
// view
// -----------------------------------------------------------
+1 -1
View File
@@ -37,7 +37,7 @@ func healthCheck(e *core.RequestEvent) error {
headersToCheck := append(
slices.Clone(e.App.Settings().TrustedProxy.Headers),
// common proxy headers
"CF-Connecting-IP", "Fly-Client-IP", "X‑Forwarded-For",
"CF-Connecting-IP", "Fly-Client-IP", "X-Forwarded-For",
)
for _, header := range headersToCheck {
if e.Request.Header.Get(header) != "" {
+46 -22
View File
@@ -84,49 +84,73 @@ func applyBodyLimit(e *core.RequestEvent, limitBytes int64) error {
}
// replace the request body
//
// note: we don't use sync.Pool since the size of the elements could vary too much
// and it might not be efficient (see https://github.com/golang/go/issues/23199)
e.Request.Body = &limitedReader{ReadCloser: e.Request.Body, limit: limitBytes}
e.Request.Body = newMaxBytesReader(e.Request.Body, limitBytes)
return nil
}
type limitedReader struct {
io.ReadCloser
limit int64
totalRead int64
func newMaxBytesReader(body io.ReadCloser, limitBytes int64) *maxBytesReader {
return &maxBytesReader{
ReadCloser: body,
limit: limitBytes,
remaining: limitBytes,
}
}
func (r *limitedReader) Read(b []byte) (int, error) {
// maxBytesReader is very similar to the http.MaxBytesReader but support
// rereads and doesn't try to prematurely close the related response
// to allow consequent middlewares to operate correctly.
type maxBytesReader struct {
io.ReadCloser
limit int64
remaining int64
stickyErr error
}
func (r *maxBytesReader) Read(b []byte) (int, error) {
if r.stickyErr != nil {
return 0, r.stickyErr
}
if len(b) == 0 {
return 0, nil
}
// if possible no need to read the entire chunk since
// remaining+1 is enough to determine whether it exceed the limit
if int64(len(b))-1 > r.remaining {
b = b[:r.remaining+1]
}
n, err := r.ReadCloser.Read(b)
if err != nil {
if int64(n) <= r.remaining {
r.remaining -= int64(n)
r.stickyErr = err
return n, err
}
r.totalRead += int64(n)
if r.totalRead > r.limit {
return n, ErrRequestEntityTooLarge
}
n = int(r.remaining)
return n, nil
r.remaining = 0
r.stickyErr = ErrRequestEntityTooLarge
return n, r.stickyErr
}
// explicit casts to ensure that the main struct methods will be invoked
// (extra precautions in case of nested interface wrapping erasure)
// ---
func (r *limitedReader) Reread() {
func (r *maxBytesReader) Reread() {
rereader, ok := r.ReadCloser.(router.Rereader)
if ok {
rereader.Reread()
r.remaining = r.limit
r.stickyErr = nil
}
}
func (r *limitedReader) Close() error {
closer, ok := r.ReadCloser.(io.Closer)
if ok {
return closer.Close()
}
return nil
func (r *maxBytesReader) Close() error {
return r.ReadCloser.Close()
}
+110 -17
View File
@@ -1,9 +1,10 @@
package apis_test
import (
"bytes"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/pocketbase/pocketbase/apis"
@@ -19,13 +20,35 @@ func TestBodyLimitMiddleware(t *testing.T) {
if err != nil {
t.Fatal(err)
}
pbRouter.POST("/a", func(e *core.RequestEvent) error {
return e.String(200, "a")
}) // default global BodyLimit check
pbRouter.POST("/b", func(e *core.RequestEvent) error {
return e.String(200, "b")
}).Bind(apis.BodyLimit(20))
testHandler := func(e *core.RequestEvent) error {
// read the body multiple times to ensure that the limited
// reader guards and rereads are invoked
var result any
if err := e.BindBody(&result); err != nil {
return err
}
if err := e.BindBody(&result); err != nil {
return err
}
return e.JSON(200, result)
}
const customLimit = 20
pbRouter.POST("/a", testHandler) // default global BodyLimit check
pbRouter.POST("/b", testHandler).Bind(apis.BodyLimit(customLimit))
pbRouter.POST("/iof", func(e *core.RequestEvent) error {
// ensure that normal io methods still operate correctly
b, err := io.ReadAll(e.Request.Body)
if err != nil {
return err
}
return e.String(http.StatusOK, string(b))
}).Bind(apis.BodyLimit(customLimit))
mux, err := pbRouter.BuildMux()
if err != nil {
@@ -33,20 +56,90 @@ func TestBodyLimitMiddleware(t *testing.T) {
}
scenarios := []struct {
url string
size int64
expectedStatus int
name string
url string
body string
lazyContentLength bool
expectedStatus int
}{
{"/a", 21, 200},
{"/a", apis.DefaultMaxBodySize + 1, 413},
{"/b", 20, 200},
{"/b", 21, 413},
{
"(eager content-length check) with body = default limit",
"/a",
`"` + strings.Repeat("a", int(apis.DefaultMaxBodySize-2)) + `"`,
false,
http.StatusOK,
},
{
"(eager content-length check) with body > default limit",
"/a",
`"` + strings.Repeat("a", int(apis.DefaultMaxBodySize)) + `"`,
false,
http.StatusRequestEntityTooLarge,
},
{
"(lazy content-length check) with body = default limit",
"/a",
`"` + strings.Repeat("a", int(apis.DefaultMaxBodySize-2)) + `"`,
true,
http.StatusOK,
},
{
"(lazy content-length check) with body > default limit",
"/a",
`"` + strings.Repeat("a", int(apis.DefaultMaxBodySize)) + `"`,
true,
http.StatusRequestEntityTooLarge,
},
// ---
{
"(eager content-length check) with body = custom limit",
"/b",
`"` + strings.Repeat("a", customLimit-2) + `"`,
false,
http.StatusOK,
},
{
"(eager content-length check) with body > custom limit",
"/b",
`"` + strings.Repeat("a", customLimit) + `"`,
false,
http.StatusRequestEntityTooLarge,
},
{
"(lazy content-length check) with body = custom limit",
"/b",
`"` + strings.Repeat("a", customLimit-2) + `"`,
true,
http.StatusOK,
},
{
"(lazy content-length check) with body > custom limit",
"/b",
`"` + strings.Repeat("a", customLimit) + `"`,
true,
http.StatusRequestEntityTooLarge,
},
// ---
{
"io.ReadAll io.EOF exact limit check",
"/iof",
`"` + strings.Repeat("a", customLimit-2) + `"`,
true,
http.StatusOK,
},
}
for _, s := range scenarios {
t.Run(fmt.Sprintf("%s_%d", s.url, s.size), func(t *testing.T) {
t.Run(s.name, func(t *testing.T) {
rec := httptest.NewRecorder()
req := httptest.NewRequest("POST", s.url, bytes.NewReader(make([]byte, s.size)))
req := httptest.NewRequest("POST", s.url, strings.NewReader(s.body))
req.Header.Set("Content-Type", "application/json")
if s.lazyContentLength {
req.ContentLength = -1
}
mux.ServeHTTP(rec, req)
result := rec.Result()
+68
View File
@@ -1,6 +1,9 @@
package core
import (
"encoding/json/v2"
"errors"
"slices"
"strconv"
"strings"
"time"
@@ -409,6 +412,71 @@ type OAuth2Config struct {
Enabled bool `form:"enabled" json:"enabled"`
}
// UnmarshalJSON implements the [json.Unmarshaler] interface.
//
// The main difference from the standard unmarshalization is that
// instead of replacing the entire providers config slice, we ensure
// that partially submitted provider data (e.g. without clientSecret)
// is merged on per config level based on the provider name
// (https://github.com/pocketbase/pocketbase/issues/7815).
func (c *OAuth2Config) UnmarshalJSON(b []byte) error {
originalProviders := slices.Clone(c.Providers)
type alias OAuth2Config
err := json.Unmarshal(b, (*alias)(c))
if err != nil {
return err
}
if len(c.Providers) == 0 {
return nil
}
// unmarshal again but this time into a plain array of objects
// so that we have only the submitted fields and no zero defaults
plain := struct {
Providers []map[string]any `json:"providers"`
}{}
err = json.Unmarshal(b, &plain)
if err != nil {
return err
}
// no providers were submitted
if len(plain.Providers) == 0 {
return nil
}
if len(c.Providers) != len(plain.Providers) {
return errors.New("the length of the plain unmarshalized providers and the ones from the config doesn't match")
}
ProvidersMergeLoop:
for i, plain := range plain.Providers {
for _, original := range originalProviders {
if original.Name == plain["name"] {
raw, err := json.Marshal(plain)
if err != nil {
return err
}
// unmarshal the new plain data on top of the original one
err = json.Unmarshal(raw, &original)
if err != nil {
return err
}
// reassigne to the updated original
c.Providers[i] = original
continue ProvidersMergeLoop
}
}
}
return nil
}
// GetProviderConfig returns the first OAuth2ProviderConfig that matches the specified name.
//
// Returns false and zero config if no such provider is available in c.Providers.
@@ -712,6 +712,76 @@ func TestPasswordAuthConfigValidate(t *testing.T) {
}
}
func TestOAuth2ConfigUnmarshalJSON(t *testing.T) {
t.Parallel()
scenarios := []struct {
name string
newJSON string
expected string
}{
{
"missing",
`{
"enabled": true,
"mappedFields": {"username": "username_test"}
}`,
`{"providers":[{"pkce":null,"name":"a","clientId":"a_clientId","clientSecret":"a_clientSecret","authURL":"","tokenURL":"","userInfoURL":"","displayName":"","extra":{}},{"pkce":null,"name":"b","clientId":"b_clientId","clientSecret":"b_clientSecret","authURL":"","tokenURL":"","userInfoURL":"","displayName":"","extra":{}}],"mappedFields":{"id":"","name":"name_test","username":"username_test","avatarURL":""},"enabled":true}`,
},
{
"empty",
`{
"enabled": true,
"mappedFields": {"username": "username_test"},
"providers": []
}`,
`{"providers":[],"mappedFields":{"id":"","name":"name_test","username":"username_test","avatarURL":""},"enabled":true}`,
},
{
"non-empty",
`{
"enabled": true,
"mappedFields": {"username": "username_test"},
"providers": [
{"name": "c", "clientId": "c_clientId", "clientSecret": "c_clientSecret"},
{"name": "a", "displayName": "a_displayName"}
]
}`,
`{"providers":[{"pkce":null,"name":"c","clientId":"c_clientId","clientSecret":"c_clientSecret","authURL":"","tokenURL":"","userInfoURL":"","displayName":"","extra":{}},{"pkce":null,"name":"a","clientId":"a_clientId","clientSecret":"a_clientSecret","authURL":"","tokenURL":"","userInfoURL":"","displayName":"a_displayName","extra":{}}],"mappedFields":{"id":"","name":"name_test","username":"username_test","avatarURL":""},"enabled":true}`,
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
config := core.OAuth2Config{
Enabled: false,
MappedFields: core.OAuth2KnownFields{
Name: "name_test",
},
Providers: []core.OAuth2ProviderConfig{
{Name: "a", ClientId: "a_clientId", ClientSecret: "a_clientSecret"},
{Name: "b", ClientId: "b_clientId", ClientSecret: "b_clientSecret"},
},
}
err := json.Unmarshal([]byte(s.newJSON), &config)
if err != nil {
t.Fatal(err)
}
raw, err := json.Marshal(config, json.Deterministic(true))
if err != nil {
t.Fatal(err)
}
rawStr := string(raw)
if rawStr != s.expected {
t.Fatalf("Expected OAuth2ProviderConfig\n%s\ngot\n%s", s.expected, rawStr)
}
})
}
}
func TestOAuth2ConfigGetProviderConfig(t *testing.T) {
scenarios := []struct {
name string
+14
View File
@@ -2,6 +2,8 @@ package core
import (
"context"
"encoding/json/jsontext"
"encoding/json/v2"
"slices"
"strconv"
"strings"
@@ -171,6 +173,18 @@ func (f *JSONField) ValidateValue(ctx context.Context, app App, record *Record)
return validation.NewError("validation_invalid_json", "Must be a valid json value")
}
// temp extra encoding/json/v2 check since the above validator is
// still using the v1 semantics
//
// @todo remove after updating the string validator
if len(raw) > 0 {
var dummy any
err := json.Unmarshal(raw, &dummy, jsontext.AllowInvalidUTF8(true))
if err != nil {
return validation.NewError("validation_invalid_json", "Must be a valid json value")
}
}
rawStr := strings.TrimSpace(raw.String())
if f.Required && slices.Contains(emptyJSONValues, rawStr) {
+10
View File
@@ -101,6 +101,16 @@ func TestJSONFieldValidateValue(t *testing.T) {
},
true,
},
{
"invalid raw value (v2 semantics)",
&core.JSONField{Name: "test"},
func() *core.Record {
record := core.NewRecord(collection)
record.SetRaw("test", types.JSONRaw(`{"a": 1, "a": 2}`))
return record
},
true,
},
{
"zero field value (not required)",
&core.JSONField{Name: "test"},
+2
View File
@@ -50,6 +50,8 @@ func (l *Log) DBExport(app App) (map[string]any, error) {
result["message"] = l.Message
}
// @todo once added in the standard library consider replacing with
// WithByteLimit and WithDepthLimit as suggested in https://github.com/golang/go/issues/56733
if len(l.Data) == 0 {
result["data"] = l.Data
} else {
+33 -13
View File
@@ -3,6 +3,8 @@ package core
import (
"bytes"
"context"
"database/sql"
"encoding/json/jsontext"
"encoding/json/v2"
"errors"
"fmt"
@@ -1329,7 +1331,14 @@ func (record *Record) PublicExport() map[string]any {
//
// Only the data exported by `PublicExport()` will be serialized.
func (m Record) MarshalJSON() ([]byte, error) {
return json.Marshal(m.PublicExport(), json.Deterministic(true))
return json.Marshal(
m.PublicExport(),
json.Deterministic(true),
// for compliance with old data (and slightly better performance)
jsontext.AllowDuplicateNames(true),
// preserve the old jsonv1 behavior in case of invalid data
jsontext.AllowInvalidUTF8(true),
)
}
// UnmarshalJSON implements the [json.Unmarshaler] interface.
@@ -1526,12 +1535,14 @@ func cascadeRecordDelete(app App, mainRecord *Record, refs map[*Collection][]Fie
continue // skip missing or view collections
}
recordTableName := inflector.Columnify(refCollection.Name)
refTableName := inflector.Columnify(refCollection.Name)
for _, field := range fields {
prefixedFieldName := recordTableName + "." + inflector.Columnify(field.GetName())
prefixedFieldName := refTableName + "." + inflector.Columnify(field.GetName())
query := app.RecordQuery(refCollection)
// fetch only the related ids because they will be queried anyway right
// before delete to ensure that we are working with fresh record data
query := app.DB().Select(refTableName + ".id").From(refTableName)
if opt, ok := field.(MultiValuer); !ok || !opt.IsMultiple() {
query.AndWhere(dbx.HashExp{prefixedFieldName: mainRecord.Id})
@@ -1545,23 +1556,24 @@ func cascadeRecordDelete(app App, mainRecord *Record, refs map[*Collection][]Fie
}
if refCollection.Id == mainRecord.Collection().Id {
query.AndWhere(dbx.Not(dbx.HashExp{recordTableName + ".id": mainRecord.Id}))
query.AndWhere(dbx.Not(dbx.HashExp{refTableName + ".id": mainRecord.Id}))
}
// trigger cascade for each batchSize rel items until there is none
batchSize := 4000
rows := make([]*Record, 0, batchSize)
batchSize := 8000
refIds := make([]string, 0, batchSize)
for {
if err := query.Limit(int64(batchSize)).All(&rows); err != nil {
err := query.Limit(int64(batchSize)).Column(&refIds)
if err != nil {
return err
}
total := len(rows)
total := len(refIds)
if total == 0 {
break
}
err := deleteRefRecords(app, mainRecord, rows, field)
err = deleteRefRecords(app, mainRecord, refCollection, refIds, field)
if err != nil {
return err
}
@@ -1570,7 +1582,7 @@ func cascadeRecordDelete(app App, mainRecord *Record, refs map[*Collection][]Fie
break // no more items
}
rows = rows[:0] // keep allocated memory
refIds = refIds[:0] // keep allocated memory
}
}
}
@@ -1583,13 +1595,21 @@ func cascadeRecordDelete(app App, mainRecord *Record, refs map[*Collection][]Fie
// just unset the record id from any relation field values (if they are not required).
//
// NB! This method is expected to be called from inside of a transaction.
func deleteRefRecords(app App, mainRecord *Record, refRecords []*Record, field Field) error {
func deleteRefRecords(app App, mainRecord *Record, refCollection *Collection, refIds []string, field Field) error {
relField, _ := field.(*RelationField)
if relField == nil {
return errors.New("only RelationField is supported at the moment, got " + field.Type())
}
for _, refRecord := range refRecords {
for _, refId := range refIds {
refRecord, err := app.FindRecordById(refCollection, refId)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
continue // already deleted
}
return err
}
ids := refRecord.GetStringSlice(relField.Name)
// unset the record id
+113 -16
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"database/sql"
"encoding/json/jsontext"
"encoding/json/v2"
"errors"
"fmt"
@@ -1412,14 +1413,15 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
f3 := &core.SelectField{Name: "field3", MaxSelect: 2, Values: []string{"test1", "test2", "test3"}}
f4 := &core.TextField{Name: "field4", Hidden: true}
f5 := &core.TextField{Name: "field5", Hidden: true}
f6 := &core.JSONField{Name: "field6"}
colBase := core.NewBaseCollection("test_base")
colBase.Id = "_pbc_base_123"
colBase.Fields.Add(f1, f2, f3, f4, f5)
colBase.Fields.Add(f1, f2, f3, f4, f5, f6)
colAuth := core.NewAuthCollection("test_auth")
colAuth.Id = "_pbc_auth_123"
colAuth.Fields.Add(f1, f2, f3, f4, f5)
colAuth.Fields.Add(f1, f2, f3, f4, f5, f6)
scenarios := []struct {
name string
@@ -1438,7 +1440,7 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
false,
nil,
nil,
`{"collectionId":"_pbc_base_123","collectionName":"test_base","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id"}`,
`{"collectionId":"_pbc_base_123","collectionName":"test_base","expand":{"test":123},"field1":"field_1�","field2":"field_2.png","field3":["test1","test2"],"field6":{"a":1,"a":2},"id":"test_id"}`,
},
{
"[base] with email visibility",
@@ -1447,7 +1449,7 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
false,
nil,
nil,
`{"collectionId":"_pbc_base_123","collectionName":"test_base","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id"}`,
`{"collectionId":"_pbc_base_123","collectionName":"test_base","expand":{"test":123},"field1":"field_1�","field2":"field_2.png","field3":["test1","test2"],"field6":{"a":1,"a":2},"id":"test_id"}`,
},
{
"[base] with custom data",
@@ -1456,7 +1458,7 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
true,
nil,
nil,
`{"collectionId":"_pbc_base_123","collectionName":"test_base","email":"test_email","emailVisibility":"test_invalid","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","password":"test_passwordHash","tokenKey":"test_tokenKey","unknown":"test_unknown","verified":true}`,
`{"collectionId":"_pbc_base_123","collectionName":"test_base","email":"test_email","emailVisibility":"test_invalid","expand":{"test":123},"field1":"field_1�","field2":"field_2.png","field3":["test1","test2"],"field6":{"a":1,"a":2},"id":"test_id","password":"test_passwordHash","tokenKey":"test_tokenKey","unknown":"test_unknown","verified":true}`,
},
{
"[base] with explicit hide and unhide fields",
@@ -1465,7 +1467,7 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
true,
[]string{"field3", "field1", "expand", "collectionId", "collectionName", "email", "tokenKey", "unknown"},
[]string{"field4", "@pbInternalAbc"},
`{"emailVisibility":"test_invalid","field2":"field_2.png","field4":"field_4","id":"test_id","password":"test_passwordHash","verified":true}`,
`{"emailVisibility":"test_invalid","field2":"field_2.png","field4":"field_4","field6":{"a":1,"a":2},"id":"test_id","password":"test_passwordHash","verified":true}`,
},
{
"[base] trying to unhide custom fields without explicit WithCustomData",
@@ -1474,7 +1476,7 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
true,
nil,
[]string{"field5", "@pbInternalAbc", "email", "tokenKey", "unknown"},
`{"collectionId":"_pbc_base_123","collectionName":"test_base","email":"test_email","emailVisibility":"test_invalid","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"field5":"field_5","id":"test_id","password":"test_passwordHash","tokenKey":"test_tokenKey","unknown":"test_unknown","verified":true}`,
`{"collectionId":"_pbc_base_123","collectionName":"test_base","email":"test_email","emailVisibility":"test_invalid","expand":{"test":123},"field1":"field_1�","field2":"field_2.png","field3":["test1","test2"],"field5":"field_5","field6":{"a":1,"a":2},"id":"test_id","password":"test_passwordHash","tokenKey":"test_tokenKey","unknown":"test_unknown","verified":true}`,
},
// auth
@@ -1485,7 +1487,7 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
false,
nil,
nil,
`{"collectionId":"_pbc_auth_123","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","verified":true}`,
`{"collectionId":"_pbc_auth_123","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1�","field2":"field_2.png","field3":["test1","test2"],"field6":{"a":1,"a":2},"id":"test_id","verified":true}`,
},
{
"[auth] with email visibility",
@@ -1494,7 +1496,7 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
false,
nil,
nil,
`{"collectionId":"_pbc_auth_123","collectionName":"test_auth","email":"test_email","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","verified":true}`,
`{"collectionId":"_pbc_auth_123","collectionName":"test_auth","email":"test_email","emailVisibility":false,"expand":{"test":123},"field1":"field_1�","field2":"field_2.png","field3":["test1","test2"],"field6":{"a":1,"a":2},"id":"test_id","verified":true}`,
},
{
"[auth] with custom data",
@@ -1503,7 +1505,7 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
true,
nil,
nil,
`{"collectionId":"_pbc_auth_123","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","unknown":"test_unknown","verified":true}`,
`{"collectionId":"_pbc_auth_123","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1�","field2":"field_2.png","field3":["test1","test2"],"field6":{"a":1,"a":2},"id":"test_id","unknown":"test_unknown","verified":true}`,
},
{
"[auth] with explicit hide and unhide fields",
@@ -1512,7 +1514,7 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
true,
[]string{"field3", "field1", "expand", "collectionId", "collectionName", "email", "unknown"},
[]string{"field4", "@pbInternalAbc"},
`{"emailVisibility":false,"field2":"field_2.png","field4":"field_4","id":"test_id","verified":true}`,
`{"emailVisibility":false,"field2":"field_2.png","field4":"field_4","field6":{"a":1,"a":2},"id":"test_id","verified":true}`,
},
{
"[auth] trying to unhide custom fields without explicit WithCustomData",
@@ -1521,17 +1523,18 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
true,
nil,
[]string{"field5", "@pbInternalAbc", "tokenKey", "unknown", "email"}, // emailVisibility:false has higher priority
`{"collectionId":"_pbc_auth_123","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"field5":"field_5","id":"test_id","unknown":"test_unknown","verified":true}`,
`{"collectionId":"_pbc_auth_123","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1�","field2":"field_2.png","field3":["test1","test2"],"field5":"field_5","field6":{"a":1,"a":2},"id":"test_id","unknown":"test_unknown","verified":true}`,
},
}
data := map[string]any{
"id": "test_id",
"field1": "field_1",
"field1": "field_1\xc3", /* invalid utf8 suffix to test mangling */
"field2": "field_2.png",
"field3": []string{"test1", "test2"},
"field4": "field_4",
"field5": "field_5",
"field6": types.JSONRaw(`{"a":1,"a":2}`), // intentionally duplicated to check serialization
"expand": map[string]any{"test": 123},
"collectionId": "m_id", // should be always ignored
"collectionName": "m_name", // should be always ignored
@@ -1554,7 +1557,12 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
m.Unhide(s.unhideFields...)
m.Hide(s.hideFields...)
exportResult, err := json.Marshal(m.PublicExport(), json.Deterministic(true))
exportResult, err := json.Marshal(
m.PublicExport(),
json.Deterministic(true),
jsontext.AllowDuplicateNames(true),
jsontext.AllowInvalidUTF8(true),
)
if err != nil {
t.Fatal(err)
}
@@ -2319,16 +2327,105 @@ func TestRecordDelete(t *testing.T) {
}
// ensure that the json rel fields were prefixed
joinedQueries := strings.Join(calledQueries, " ")
expectedRelManyPart := "SELECT `demo1`.* FROM `demo1` WHERE EXISTS (SELECT 1 FROM json_each(CASE WHEN iif(json_valid([[demo1.rel_many]]), json_type([[demo1.rel_many]])='array', FALSE) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END) {{__je__}} WHERE [[__je__.value]]='"
expectedRelManyPart := "SELECT `demo1`.`id` FROM `demo1` WHERE EXISTS (SELECT 1 FROM json_each(CASE WHEN iif(json_valid([[demo1.rel_many]]), json_type([[demo1.rel_many]])='array', FALSE) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END) {{__je__}} WHERE [[__je__.value]]='"
if !strings.Contains(joinedQueries, expectedRelManyPart) {
t.Fatalf("(rec3) Expected the cascade delete to call the query \n%v, got \n%v", expectedRelManyPart, calledQueries)
}
expectedRelOnePart := "SELECT `demo1`.* FROM `demo1` WHERE (`demo1`.`rel_one`='"
expectedRelOnePart := "SELECT `demo1`.`id` FROM `demo1` WHERE (`demo1`.`rel_one`='"
if !strings.Contains(joinedQueries, expectedRelOnePart) {
t.Fatalf("(rec3) Expected the cascade delete to call the query \n%v, got \n%v", expectedRelOnePart, calledQueries)
}
}
func TestRecordDeleteWithMultipleRelationCascade(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
// create a mock collection with self referencing multiple relation field
// ---
collection := core.NewBaseCollection("test")
err := app.Save(collection)
if err != nil {
t.Fatal(err)
}
// for simpler mocks
collection.Fields.GetByName("id").(*core.TextField).Min = 1
collection.Fields.Add(&core.RelationField{
Name: "rels",
CollectionId: collection.Id,
MaxSelect: 99,
CascadeDelete: true,
})
err = app.Save(collection)
if err != nil {
t.Fatal(err)
}
// create mock records
// ---
relsData := map[string][]string{
"a": nil,
"b": {"a"},
"c": {"a", "b"},
"d": {},
"e": {"c", "d"},
}
for id, rels := range relsData {
record := core.NewRecord(collection)
record.Set("id", id)
record.Set("rels", rels)
err = app.SaveNoValidate(record) // map is not ordered
if err != nil {
t.Fatalf("failed to create mock record: %v", err)
}
}
// trigger cascade delete for the top record
// ---
aRecord, err := app.FindRecordById(collection, "a")
if err != nil {
t.Fatal(err)
}
err = app.Delete(aRecord)
if err != nil {
t.Fatal(err)
}
// verify cascade delete
// ---
expectedRels := map[string][]string{
"d": {},
"e": {"d"},
}
allRecords, err := app.FindAllRecords(collection)
if err != nil {
t.Fatal(err)
}
if len(allRecords) != len(expectedRels) {
t.Fatalf("Expected %d remaining records, got %d", len(expectedRels), len(allRecords))
}
for _, r := range allRecords {
expected, ok := expectedRels[r.Id]
if !ok {
t.Fatalf("Record %q wasn't found in %v", r.Id, expectedRels)
}
rels := r.GetStringSlice("rels")
if !slices.Equal(rels, expected) {
t.Fatalf("Record %q expected rels\n%v\ngot\n%v", r.Id, expected, rels)
}
}
}
func TestRecordDeleteBatchProcessing(t *testing.T) {
t.Parallel()
-27
View File
@@ -581,18 +581,6 @@ type TrustedProxyConfig struct {
UseLeftmostIP bool `form:"useLeftmostIP" json:"useLeftmostIP"`
}
// MarshalJSON implements the [json.Marshaler] interface.
func (c TrustedProxyConfig) MarshalJSON() ([]byte, error) {
type alias TrustedProxyConfig
// serialize as empty array
if c.Headers == nil {
c.Headers = []string{}
}
return json.Marshal(alias(c))
}
// Validate makes RateLimitRule validatable by implementing [validation.Validatable] interface.
func (c TrustedProxyConfig) Validate() error {
return nil
@@ -640,21 +628,6 @@ func (c *RateLimitsConfig) FindRateLimitRule(searchLabels []string, optOnlyAudie
return RateLimitRule{}, false
}
// MarshalJSON implements the [json.Marshaler] interface.
func (c RateLimitsConfig) MarshalJSON() ([]byte, error) {
type alias RateLimitsConfig
// serialize as empty array
if c.Rules == nil {
c.Rules = []RateLimitRule{}
}
if c.ExcludedIPs == nil {
c.ExcludedIPs = []string{}
}
return json.Marshal(alias(c))
}
// Validate makes RateLimitsConfig validatable by implementing [validation.Validatable] interface.
func (c RateLimitsConfig) Validate() error {
return validation.ValidateStruct(&c,
+3 -3
View File
@@ -5,7 +5,7 @@ go 1.27
require (
github.com/disintegration/imaging v1.6.2
github.com/domodwyer/mailyak/v3 v3.6.2
github.com/dop251/goja v0.0.0-20260806115107-493f22071ef6
github.com/dop251/goja v0.0.0-20260901132549-43234fa61381
github.com/dop251/goja_nodejs v0.0.0-20260212111938-1f56ff5bcf14
github.com/fatih/color v1.19.0
github.com/fsnotify/fsnotify v1.10.1
@@ -17,7 +17,7 @@ require (
github.com/pocketbase/tygoja v0.1.0
github.com/spf13/cast v1.10.0
github.com/spf13/cobra v1.10.2
golang.org/x/crypto v0.55.0
golang.org/x/crypto v0.56.0
golang.org/x/image v0.45.0
golang.org/x/net v0.58.0
golang.org/x/oauth2 v0.36.0
@@ -31,7 +31,7 @@ require (
github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 // indirect
github.com/google/pprof v0.0.0-20260902005441-ca85771921e4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
+6 -6
View File
@@ -15,8 +15,8 @@ github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCO
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 h1:16iT9CBDOniJwFGPI41MbUDfEk74hFaKTqudrX8kenY=
github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217/go.mod h1:eIb+f24U+eWQCIsj9D/ah+MD9UP+wdxuqzsdLD+mhGM=
github.com/dop251/goja v0.0.0-20260806115107-493f22071ef6 h1:Oh2rRG1un7tLlC3/NJDzKppZ4CeZGkVFJCUOTRwLpfw=
github.com/dop251/goja v0.0.0-20260806115107-493f22071ef6/go.mod h1:LiIEzozrcvNXorsG/3+ypGqdTUAqZryhzSsqi0oU/Qg=
github.com/dop251/goja v0.0.0-20260901132549-43234fa61381 h1:vzmJ+R9qxuaksCAB+Kn6wtGgQ01cIfKzUGwWWcX5zXI=
github.com/dop251/goja v0.0.0-20260901132549-43234fa61381/go.mod h1:u8yZRUavu+N4EnFFy6J5fVtjE7lEcZ2YyV2GcBXY9c8=
github.com/dop251/goja_nodejs v0.0.0-20260212111938-1f56ff5bcf14 h1:3U8dTgyNBhEQ/GVw0jZW5q+93Zw2gAZPRWhJ9TwV3rM=
github.com/dop251/goja_nodejs v0.0.0-20260212111938-1f56ff5bcf14/go.mod h1:Tb7Xxye4LX7cT3i8YLvmPMGCV92IOi4CDZvm/V8ylc0=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@@ -42,8 +42,8 @@ github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
github.com/google/pprof v0.0.0-20260902005441-ca85771921e4 h1:/6mPXfWmhv8eKck12I0YNIcIjwHtxP3YRIMKiEgTjWg=
github.com/google/pprof v0.0.0-20260902005441-ca85771921e4/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
@@ -88,8 +88,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -129,7 +129,7 @@ type excludeHooks<Type> = {
type CoreApp = excludeHooks<ORIGINAL_CORE_APP>
// pocketbase.PocketBase without the on* hook methods
type PocketBase = excludeHooks<ORIGINAL_POCKETBASE>
interface PocketBase extends excludeHooks<ORIGINAL_POCKETBASE>{}
/**
* ` + "`$app`" + ` is the current running PocketBase instance that is globally
+2 -14
View File
@@ -8,7 +8,6 @@ import (
"fmt"
"path/filepath"
"slices"
"strconv"
"strings"
"github.com/pocketbase/pocketbase/core"
@@ -667,22 +666,11 @@ func init() {
}
func marhshalWithoutEscape(v any, prefix string, indent string) ([]byte, error) {
raw, err := json.Marshal(v,
json.Deterministic(true),
return json.Marshal(v,
jsontext.WithIndentPrefix(prefix),
jsontext.WithIndent(indent),
json.Deterministic(true),
)
if err != nil {
return nil, err
}
// unescape escaped unicode characters
unescaped, err := strconv.Unquote(strings.ReplaceAll(strconv.Quote(string(raw)), `\\u`, `\u`))
if err != nil {
return nil, err
}
return []byte(unescaped), nil
}
func escapeBacktick(v string) string {
+2 -2
View File
@@ -131,9 +131,9 @@ func (p *OIDC) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) {
}
func (p *OIDC) parseIdToken(token *oauth2.Token) (jwt.MapClaims, error) {
idToken := token.Extra("id_token").(string)
idToken, _ := token.Extra("id_token").(string)
if idToken == "" {
return nil, errors.New("empty id_token")
return nil, errors.New("empty or invalid id_token")
}
claims := jwt.MapClaims{}
+11 -5
View File
@@ -8,8 +8,8 @@ import (
)
var (
indexRegex = regexp.MustCompile(`(?im)create\s+(unique\s+)?\s*index\s*(if\s+not\s+exists\s+)?(\S*)\s+on\s+(\S*)\s*\(([\s\S]*)\)(?:\s*where\s+([\s\S]*))?`)
indexColumnRegex = regexp.MustCompile(`(?im)^([\s\S]+?)(?:\s+collate\s+([\w]+))?(?:\s+(asc|desc))?$`)
indexRegex = regexp.MustCompile(`(?i)\s*create\s+(unique\s+)?\s*index\s*(if\s+not\s+exists\s+)?(\S*)\s+on\s+(\S*)\s*\(([\s\S]*?)\)(?:\s+where\s+([\s\S]*?))?\s*$`)
indexColumnRegex = regexp.MustCompile(`(?i)^([\s\S]+?)(?:\s+collate\s+([\w]+))?(?:\s+(asc|desc))?\s*$`)
)
// IndexColumn represents a single parsed SQL index column.
@@ -148,11 +148,12 @@ func ParseIndex(createIndexExpr string) Index {
nameTk.Separators('.')
nameParts, _ := nameTk.ScanAll()
if len(nameParts) == 2 {
switch len(nameParts) {
case 1:
result.IndexName = strings.Trim(nameParts[0], trimChars)
case 2:
result.SchemaName = strings.Trim(nameParts[0], trimChars)
result.IndexName = strings.Trim(nameParts[1], trimChars)
} else {
result.IndexName = strings.Trim(nameParts[0], trimChars)
}
// TableName
@@ -186,6 +187,11 @@ func ParseIndex(createIndexExpr string) Index {
})
}
if len(rawColumns) != len(result.Columns) {
// unset to trigger validation error
result.Columns = []IndexColumn{}
}
// WHERE expression
// ---
result.Where = strings.TrimSpace(matches[6])
+22 -12
View File
@@ -3,7 +3,6 @@ package dbutils_test
import (
"bytes"
"encoding/json/v2"
"fmt"
"strings"
"testing"
@@ -12,17 +11,28 @@ import (
func TestParseIndex(t *testing.T) {
scenarios := []struct {
name string
index string
expected dbutils.Index
}{
// invalid
{
"invalid",
`invalid`,
dbutils.Index{},
},
// simple (multiple spaces between the table and columns list)
{
`create index indexname on tablename (col1)`,
"no names",
`create index on ()`,
dbutils.Index{},
},
{
"invalid index name",
`create index a.b.c on ()`,
dbutils.Index{},
},
{
"simple (multiple spaces as prefix, suffix, between the table and columns list)",
` create index indexname on tablename (col1) `,
dbutils.Index{
IndexName: "indexname",
TableName: "tablename",
@@ -31,8 +41,8 @@ func TestParseIndex(t *testing.T) {
},
},
},
// simple (no space between the table and the columns list)
{
"simple (no space between the table and the columns list)",
`create index indexname on tablename(col1)`,
dbutils.Index{
IndexName: "indexname",
@@ -42,15 +52,15 @@ func TestParseIndex(t *testing.T) {
},
},
},
// all fields
{
"all fields",
`CREATE UNIQUE INDEX IF NOT EXISTS "schemaname".[indexname] on 'tablename' (
col0,
` + "`" + `col1` + "`" + `,
json_extract("col2", "$.a") asc,
json_extract("col2_multiline",` + "\n" + ` "$.a") asc,
"col3" collate NOCASE,
"col4" collate RTRIM desc
) where test = 1`,
) where cast(test1 as ` + "\n" + `int) = 1 and test2 != ''`,
dbutils.Index{
Unique: true,
Optional: true,
@@ -60,17 +70,17 @@ func TestParseIndex(t *testing.T) {
Columns: []dbutils.IndexColumn{
{Name: "col0"},
{Name: "col1"},
{Name: `json_extract("col2", "$.a")`, Sort: "ASC"},
{Name: `json_extract("col2_multiline",` + "\n" + ` "$.a")`, Sort: "ASC"},
{Name: `col3`, Collate: "NOCASE"},
{Name: `col4`, Collate: "RTRIM", Sort: "DESC"},
},
Where: "test = 1",
Where: "cast(test1 as \nint) = 1 and test2 != ''",
},
},
}
for i, s := range scenarios {
t.Run(fmt.Sprintf("scenario_%d", i), func(t *testing.T) {
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result := dbutils.ParseIndex(s.index)
resultRaw, err := json.Marshal(result)
+32 -6
View File
@@ -1,13 +1,18 @@
package picker
import (
"encoding/json/jsontext"
"encoding/json/v2"
"errors"
"fmt"
"strings"
"github.com/pocketbase/pocketbase/tools/search"
"github.com/pocketbase/pocketbase/tools/tokenizer"
)
var ErrInvalidModifierData = errors.New("failed to apply some of the field modifiers for the provided data")
// Pick converts data into a []any, map[string]any, etc. (using json marshal->unmarshal)
// containing only the fields from the parsed rawFields expression.
//
@@ -15,6 +20,10 @@ import (
// Nested fields should be listed with dot-notation.
// Fields value modifiers are also supported using the `:modifier(args)` format (see Modifiers).
//
// In case some data fails to apply against the registered modifiers
// a wrapped [ErrInvalidModifierData] is returned that you can inspect and
// decide to ignore or propagate further up the execution chain.
//
// Example:
//
// data := map[string]any{"a": 1, "b": 2, "c": map[string]any{"c1": 11, "c2": 22}}
@@ -29,15 +38,23 @@ func Pick(data any, rawFields string) (any, error) {
// implementations are invoked, and then convert it back to a plain
// json value that we can further operate on.
//
// Note that invalid UTF8 characters are mangled for compatibility
// with earlier versions and to prevent unnecessary causing an error.
//
// Duplicated keys are also enabled for just in case of old jsonv1 data
// (new db entries are usually validated against the jsonv2 semantics).
//
// @todo research other approaches to avoid the double serialization
// ---
encoded, err := json.Marshal(data)
encoded, err := json.Marshal(data, jsontext.AllowDuplicateNames(true), jsontext.AllowInvalidUTF8(true))
if err != nil {
return nil, err
}
var decoded any
if err := json.Unmarshal(encoded, &decoded); err != nil {
err = json.Unmarshal(encoded, &decoded, jsontext.AllowDuplicateNames(true))
if err != nil {
return nil, err
}
// ---
@@ -51,10 +68,16 @@ func Pick(data any, rawFields string) (any, error) {
if isSearchResult {
if decodedMap, ok := decoded.(map[string]any); ok {
pickParsedFields(decodedMap["items"], parsedFields)
err = pickParsedFields(decodedMap["items"], parsedFields)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidModifierData, err)
}
}
} else {
pickParsedFields(decoded, parsedFields)
err = pickParsedFields(decoded, parsedFields)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidModifierData, err)
}
}
return decoded, nil
@@ -90,7 +113,10 @@ func parseFields(rawFields string) (map[string]Modifier, error) {
func pickParsedFields(data any, fields map[string]Modifier) error {
switch v := data.(type) {
case map[string]any:
pickMapFields(v, fields)
err := pickMapFields(v, fields)
if err != nil {
return err
}
case []map[string]any:
for _, item := range v {
if err := pickMapFields(item, fields); err != nil {
@@ -108,7 +134,7 @@ func pickParsedFields(data any, fields map[string]Modifier) error {
for _, item := range v {
if err := pickMapFields(item.(map[string]any), fields); err != nil {
return nil
return err
}
}
}
+37 -3
View File
@@ -1,13 +1,28 @@
package picker_test
import (
"encoding/json/jsontext"
"encoding/json/v2"
"errors"
"testing"
"github.com/pocketbase/pocketbase/tools/picker"
"github.com/pocketbase/pocketbase/tools/search"
)
type brokenModifier struct {
}
func (m *brokenModifier) Modify(val any) (any, error) {
return nil, errors.New("test_error")
}
func init() {
picker.Modifiers["broken"] = func(args ...string) (picker.Modifier, error) {
return &brokenModifier{}, nil
}
}
func TestPickFields(t *testing.T) {
scenarios := []struct {
name string
@@ -62,11 +77,11 @@ func TestPickFields(t *testing.T) {
"slice of maps with existing and missing fields",
[]any{
map[string]any{"a": 11, "b": 11, "c": "test1"},
map[string]any{"a": 22, "b": 22, "c": "test2"},
map[string]any{"a": 22, "b": 22, "c": "\xc3" /* test invalid utf8 mangling */},
},
"a, c ,missing", // test individual fields trim
false,
`[{"a":11,"c":"test1"},{"a":22,"c":"test2"}]`,
`[{"a":11,"c":"test1"},{"a":22,"c":"�"}]`,
},
{
"nested fields with mixed map and any slices",
@@ -223,6 +238,13 @@ func TestPickFields(t *testing.T) {
false,
`{"id":"123","rel":{"id":"456","sub":{"id":"789"},"title":"rel_title"}}`,
},
{
"with modifer.Modify error",
map[string]any{"a": 1},
"*:broken",
true,
`{"a":1}`,
},
{
"invalid excerpt modifier",
map[string]any{"a": 1, "b": 2, "c": "test"},
@@ -248,6 +270,13 @@ func TestPickFields(t *testing.T) {
false,
`{"id":"12","rel":{"title":"rel..."},"title":"lo"}`,
},
{
"jsonv1 values",
map[string]any{"a": "test\xc3", "b": jsontext.Value(`{"a":1,"a":2}`)},
"a, b",
false,
`{"a":"test�","b":{"a":2}}`,
},
}
for _, s := range scenarios {
@@ -263,7 +292,12 @@ func TestPickFields(t *testing.T) {
return
}
serialized, err := json.Marshal(result, json.Deterministic(true))
serialized, err := json.Marshal(
result,
json.Deterministic(true),
jsontext.AllowInvalidUTF8(true),
jsontext.AllowDuplicateNames(true),
)
if err != nil {
t.Fatal(err)
}
+25 -14
View File
@@ -185,24 +185,35 @@ const jsonFieldsParam = "fields"
// It also provides a generic response data fields picker if the "fields" query parameter is set.
// For example, if you are requesting `?fields=a,b` for `e.JSON(200, map[string]int{ "a":1, "b":2, "c":3 })`,
// it should result in a JSON response like: `{"a":1, "b": 2}`.
//
// Note that invalid UTF8 characters are mangled for compatibility
// with earlier versions and to prevent unnecessary causing a response error.
func (e *Event) JSON(status int, data any) error {
// try to pick only the requested fields (currently allowed only for "success" responses)
rawFields := e.Request.URL.Query().Get(jsonFieldsParam)
if rawFields != "" && status >= 200 && status <= 299 {
modified, err := picker.Pick(data, rawFields)
if err == nil {
data = modified
} else if !errors.Is(err, picker.ErrInvalidModifierData) {
// @todo ignore for now modifier data errors to avoid introducing
// breaking changes but once the router is merged with core consider
// at least logging for dev purposes
return err
}
}
e.setResponseHeaderIfEmpty(headerContentType, "application/json")
e.Response.WriteHeader(status)
rawFields := e.Request.URL.Query().Get(jsonFieldsParam)
// error response or no fields to pick
if rawFields == "" || status < 200 || status > 299 {
return json.MarshalWrite(e.Response, data)
}
// pick only the requested fields
modified, err := picker.Pick(data, rawFields)
if err != nil {
return err
}
return json.MarshalWrite(e.Response, modified)
return json.MarshalWrite(
e.Response,
data,
// for compliance with old data (and slightly better performance)
jsontext.AllowDuplicateNames(true),
// preserve the old jsonv1 behavior in case of invalid data
jsontext.AllowInvalidUTF8(true),
)
}
// XML writes an XML response.
+141 -4
View File
@@ -3,6 +3,7 @@ package router_test
import (
"bytes"
"crypto/tls"
"encoding/json/jsontext"
"encoding/json/v2"
"encoding/xml"
"errors"
@@ -19,7 +20,9 @@ import (
"testing"
validation "github.com/pocketbase/ozzo-validation/v4"
"github.com/pocketbase/pocketbase/tools/picker"
"github.com/pocketbase/pocketbase/tools/router"
"github.com/pocketbase/pocketbase/tools/types"
)
type unwrapTester struct {
@@ -220,6 +223,8 @@ func TestEventRemoteIP(t *testing.T) {
}
func TestFindUploadedFiles(t *testing.T) {
t.Parallel()
scenarios := []struct {
filename string
expectedPattern string
@@ -273,6 +278,8 @@ func TestFindUploadedFiles(t *testing.T) {
}
func TestFindUploadedFilesMissing(t *testing.T) {
t.Parallel()
body := new(bytes.Buffer)
mp := multipart.NewWriter(body)
mp.Close()
@@ -293,6 +300,8 @@ func TestFindUploadedFilesMissing(t *testing.T) {
}
func TestEventSetGet(t *testing.T) {
t.Parallel()
event := router.Event{}
// get before any set (ensures that doesn't panic)
@@ -324,6 +333,8 @@ func TestEventSetGet(t *testing.T) {
}
func TestEventSetAllGetAll(t *testing.T) {
t.Parallel()
data := map[string]any{
"a": 123,
"b": 456,
@@ -351,6 +362,8 @@ func TestEventSetAllGetAll(t *testing.T) {
}
func TestEventString(t *testing.T) {
t.Parallel()
scenarios := []testResponseWriteScenario[string]{
{
name: "no explicit content-type",
@@ -380,6 +393,8 @@ func TestEventString(t *testing.T) {
}
func TestEventHTML(t *testing.T) {
t.Parallel()
scenarios := []testResponseWriteScenario[string]{
{
name: "no explicit content-type",
@@ -409,9 +424,17 @@ func TestEventHTML(t *testing.T) {
}
func TestEventJSON(t *testing.T) {
body := map[string]any{"a": 123, "b": 456, "c": "test"}
t.Parallel()
body := map[string]any{
"a": 123,
"b": true,
"c": "test",
"d": "\xc3", // invalid utf8 char to test mangling
"e": types.JSONRaw(`{"a":1,"a":2}`), // duplicated keys to ensure compliance with old data
}
expectedPickedBody := `{"a":123,"c":"test"}`
expectedFullBody := `{"a":123,"b":456,"c":"test"}`
expectedFullBody := `{"a":123,"b":true,"c":"test","d":"�","e":{"a":2}}`
scenarios := []testResponseWriteScenario[any]{
{
@@ -451,7 +474,93 @@ func TestEventJSON(t *testing.T) {
}
}
func TestEventJSONPickError(t *testing.T) {
t.Parallel()
req, err := http.NewRequest(http.MethodGet, "/?fields=a:excerpt(-1)", nil)
if err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
event := &router.Event{
Request: req,
Response: &router.ResponseWriter{ResponseWriter: rec},
}
err = event.JSON(200, map[string]any{"a": "test"})
if err == nil {
t.Fatal("Expected JSON to return modifier args err, got nil")
}
// ensure that no explicit status code was written yet by attempting to write one
// (should do nothing if it was already written)
rec.WriteHeader(567)
result := rec.Result()
defer result.Body.Close()
if result.StatusCode != 567 {
t.Fatalf("Expected custom status code, got %d", result.StatusCode)
}
}
type brokenModifier struct {
}
func (m *brokenModifier) Modify(val any) (any, error) {
return nil, errors.New("test_error")
}
func TestEventJSONIgnoredError(t *testing.T) {
t.Parallel()
picker.Modifiers["broken_ignore"] = func(args ...string) (picker.Modifier, error) {
return &brokenModifier{}, nil
}
req, err := http.NewRequest(http.MethodGet, "/?fields=a:broken_ignore", nil)
if err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
event := &router.Event{
Request: req,
Response: &router.ResponseWriter{ResponseWriter: rec},
}
err = event.JSON(200, map[string]any{"a": "test"})
if err != nil {
t.Fatalf("Expected nil JSON result, got error %v", err)
}
// (should do nothing if it was already written)
rec.WriteHeader(567)
result := rec.Result()
defer result.Body.Close()
rawBody, err := io.ReadAll(result.Body)
if err != nil {
t.Fatal(err)
}
if result.StatusCode != http.StatusOK {
t.Fatalf("Expected %d status code, got %d", http.StatusOK, result.StatusCode)
}
expected := `{"a":"test"}`
if string(rawBody) != expected {
t.Fatalf("Expected json\n%s\ngot\n%s", expected, rawBody)
}
}
func TestEventXML(t *testing.T) {
t.Parallel()
scenarios := []testResponseWriteScenario[string]{
{
name: "no explicit content-type",
@@ -481,6 +590,8 @@ func TestEventXML(t *testing.T) {
}
func TestEventStream(t *testing.T) {
t.Parallel()
scenarios := []testResponseWriteScenario[string]{
{
name: "stream",
@@ -501,6 +612,8 @@ func TestEventStream(t *testing.T) {
}
func TestEventBlob(t *testing.T) {
t.Parallel()
scenarios := []testResponseWriteScenario[[]byte]{
{
name: "blob",
@@ -521,6 +634,8 @@ func TestEventBlob(t *testing.T) {
}
func TestEventNoContent(t *testing.T) {
t.Parallel()
s := testResponseWriteScenario[any]{
name: "no content",
status: 234,
@@ -537,6 +652,8 @@ func TestEventNoContent(t *testing.T) {
}
func TestEventFlush(t *testing.T) {
t.Parallel()
rec := httptest.NewRecorder()
event := &router.Event{
@@ -551,6 +668,8 @@ func TestEventFlush(t *testing.T) {
}
func TestEventRedirect(t *testing.T) {
t.Parallel()
scenarios := []testResponseWriteScenario[any]{
{
name: "non-30x status",
@@ -575,6 +694,8 @@ func TestEventRedirect(t *testing.T) {
}
func TestEventFileFS(t *testing.T) {
t.Parallel()
// stub test files
// ---
dir, err := os.MkdirTemp("", "EventFileFS")
@@ -671,6 +792,8 @@ func TestEventFileFS(t *testing.T) {
}
func TestEventError(t *testing.T) {
t.Parallel()
err := new(router.Event).Error(123, "message_test", map[string]any{"a": validation.Required, "b": "test"})
result, _ := json.Marshal(err, json.Deterministic(true))
@@ -682,6 +805,8 @@ func TestEventError(t *testing.T) {
}
func TestEventBadRequestError(t *testing.T) {
t.Parallel()
err := new(router.Event).BadRequestError("message_test", map[string]any{"a": validation.Required, "b": "test"})
result, _ := json.Marshal(err, json.Deterministic(true))
@@ -693,6 +818,8 @@ func TestEventBadRequestError(t *testing.T) {
}
func TestEventNotFoundError(t *testing.T) {
t.Parallel()
err := new(router.Event).NotFoundError("message_test", map[string]any{"a": validation.Required, "b": "test"})
result, _ := json.Marshal(err, json.Deterministic(true))
@@ -704,6 +831,8 @@ func TestEventNotFoundError(t *testing.T) {
}
func TestEventForbiddenError(t *testing.T) {
t.Parallel()
err := new(router.Event).ForbiddenError("message_test", map[string]any{"a": validation.Required, "b": "test"})
result, _ := json.Marshal(err, json.Deterministic(true))
@@ -715,6 +844,8 @@ func TestEventForbiddenError(t *testing.T) {
}
func TestEventUnauthorizedError(t *testing.T) {
t.Parallel()
err := new(router.Event).UnauthorizedError("message_test", map[string]any{"a": validation.Required, "b": "test"})
result, _ := json.Marshal(err, json.Deterministic(true))
@@ -726,6 +857,8 @@ func TestEventUnauthorizedError(t *testing.T) {
}
func TestEventTooManyRequestsError(t *testing.T) {
t.Parallel()
err := new(router.Event).TooManyRequestsError("message_test", map[string]any{"a": validation.Required, "b": "test"})
result, _ := json.Marshal(err, json.Deterministic(true))
@@ -737,6 +870,8 @@ func TestEventTooManyRequestsError(t *testing.T) {
}
func TestEventInternalServerError(t *testing.T) {
t.Parallel()
err := new(router.Event).InternalServerError("message_test", map[string]any{"a": validation.Required, "b": "test"})
result, _ := json.Marshal(err, json.Deterministic(true))
@@ -748,6 +883,8 @@ func TestEventInternalServerError(t *testing.T) {
}
func TestEventBindBody(t *testing.T) {
t.Parallel()
type testDstStruct struct {
A int `json:"a" xml:"a" form:"a"`
B int `json:"b" xml:"b" form:"b"`
@@ -940,9 +1077,9 @@ func testEventResponseWrite[T any](
// try to deserialize into a map and then serialize again in a
// deterministic manner in case it is json
var jsonBody any
err = json.Unmarshal(rawBody, &jsonBody)
err = json.Unmarshal(rawBody, &jsonBody, jsontext.AllowDuplicateNames(true))
if err == nil {
normalized, err := json.Marshal(jsonBody, json.Deterministic(true))
normalized, err := json.Marshal(jsonBody, json.Deterministic(true), jsontext.AllowDuplicateNames(true))
if err != nil {
t.Fatalf("Failed to deterministicly serialize json body\n%s\ngot\n%v", jsonBody, err)
}
+8 -2
View File
@@ -2,6 +2,7 @@ package router
import (
"bufio"
"encoding/json/jsontext"
"encoding/json/v2"
"errors"
"io"
@@ -176,8 +177,13 @@ func ErrorHandler(resp http.ResponseWriter, req *http.Request, err error) {
resp.WriteHeader(apiErr.Status)
if req.Method != http.MethodHead {
// note: deterministic because some logs may depend on the exact serialized error
jsonErr := json.MarshalWrite(resp, apiErr, json.Deterministic(true))
jsonErr := json.MarshalWrite(
resp,
apiErr,
// note: deterministic because some logs may depend on the exact serialized error
json.Deterministic(true),
jsontext.AllowInvalidUTF8(false),
)
if jsonErr != nil {
log.Println(jsonErr) // truly rare case, log to stderr only for dev purposes
}
+28 -16
View File
@@ -56,27 +56,39 @@ func (f FilterData) BuildExprWithLimit(
raw := string(f)
// replace the placeholder params in the raw string filter
for _, p := range placeholderReplacements {
for key, value := range p {
var replacement string
switch v := value.(type) {
case nil:
replacement = "null"
case bool, float64, float32, int, int64, int32, int16, int8, uint, uint64, uint32, uint16, uint8:
replacement = cast.ToString(v)
default:
replacement = cast.ToString(v)
if len(placeholderReplacements) > 0 {
replacements := make([]string, 0, len(placeholderReplacements[0])*2)
// try to json serialize as fallback
if replacement == "" {
raw, _ := json.Marshal(v, json.Deterministic(true))
replacement = string(raw)
for _, p := range placeholderReplacements {
for key, value := range p {
var replacement string
switch v := value.(type) {
case nil:
replacement = "null"
case bool, float64, float32, int, int64, int32, int16, int8, uint, uint64, uint32, uint16, uint8:
replacement = cast.ToString(v)
default:
casted, err := cast.ToStringE(v)
// try to json serialize as fallback
if err != nil {
fallback, err := json.Marshal(v, json.Deterministic(true))
if err != nil {
return nil, fmt.Errorf("failed to serialize param %q: %w", key, err)
}
casted = string(fallback)
}
replacement = strconv.Quote(casted)
}
replacement = strconv.Quote(replacement)
replacements = append(replacements, "{:"+key+"}", replacement)
}
raw = strings.ReplaceAll(raw, "{:"+key+"}", replacement)
}
replacer := strings.NewReplacer(replacements...)
raw = replacer.Replace(raw)
}
cacheKey := raw + "/" + strconv.Itoa(maxExpressions)
+35 -2
View File
@@ -3,6 +3,7 @@ package search_test
import (
"context"
"database/sql"
"errors"
"fmt"
"regexp"
"strings"
@@ -143,7 +144,7 @@ func TestFilterDataBuildExpr(t *testing.T) {
"geoDistance function",
"geoDistance(1,2,3,4) < 567",
false,
"(6371 * acos(cos(radians({:TEST})) * cos(radians({:TEST})) * cos(radians({:TEST}) - radians({:TEST})) + sin(radians({:TEST})) * sin(radians({:TEST})))) < {:TEST}",
"(6371 * acos(min(1, max(-1, cos(radians({:TEST})) * cos(radians({:TEST})) * cos(radians({:TEST}) - radians({:TEST})) + sin(radians({:TEST})) * sin(radians({:TEST})))))) < {:TEST}",
},
}
@@ -242,13 +243,45 @@ func TestFilterDataBuildExprWithParams(t *testing.T) {
t.Fatalf("Expected 1 query, got %d", len(calledQueries))
}
expectedQuery := `SELECT * WHERE ([[test1]] = 1 OR [[test2]] = 0 OR [[test3a]] = 123.456 OR [[test3b]] = 123.456 OR ([[test4]] = '' OR [[test4]] IS NULL) OR [[test5]] = '""' OR [[test6]] = 'simple' OR [[test7]] = '''single_quotes''' OR [[test8]] = '"double_quotes"' OR [[test9]] = '''"quote_with_backslash\' OR [[test10]] = '2023-01-01 00:00:00 +0000 UTC' OR [[test11]] = '["a","''quote","\"quote"]' OR [[test12]] = '{"a":123,"b":"quote\""}' OR [[test13]] = 'a`
expectedQuery := `SELECT * WHERE ([[test1]] = 1 OR [[test2]] = 0 OR [[test3a]] = 123.456 OR [[test3b]] = 123.456 OR ([[test4]] = '' OR [[test4]] IS NULL) OR ([[test5]] = '' OR [[test5]] IS NULL) OR [[test6]] = 'simple' OR [[test7]] = '''single_quotes''' OR [[test8]] = '"double_quotes"' OR [[test9]] = '''"quote_with_backslash\' OR [[test10]] = '2023-01-01 00:00:00 +0000 UTC' OR [[test11]] = '["a","''quote","\"quote"]' OR [[test12]] = '{"a":123,"b":"quote\""}' OR [[test13]] = 'a`
expectedQuery += "\nb')"
if expectedQuery != calledQueries[0] {
t.Fatalf("Expected query \n%s, \ngot \n%s", expectedQuery, calledQueries[0])
}
}
type brokenJSON struct{}
func (j brokenJSON) MarshalJSON() ([]byte, error) {
return nil, errors.New("test_error")
}
func TestFilterDataBuildExprWithParamsFallbackError(t *testing.T) {
t.Parallel()
resolver := search.NewSimpleFieldResolver("test")
filter := search.FilterData(`test = {:test}`)
t.Run("non-string type but valid json", func(t *testing.T) {
_, err := filter.BuildExpr(resolver, dbx.Params{
"test": map[string]any{"a": "123"},
})
if err != nil {
t.Fatal(err)
}
})
t.Run("non-string type but invalid json", func(t *testing.T) {
_, err := filter.BuildExpr(resolver, dbx.Params{
"test": brokenJSON{},
})
if err == nil {
t.Fatal("Expected filter build error, got nil")
}
})
}
func TestFilterDataBuildExprWithLimit(t *testing.T) {
resolver := search.NewSimpleFieldResolver(`^\w+$`)
+5 -2
View File
@@ -49,11 +49,14 @@ var TokenFunctions = map[string]func(
return &ResolverResult{
NullFallback: NullFallbackDisabled,
Identifier: `(6371 * acos(` +
// the clamping is to prevent floating point rounding errors for values like
// "1.0002" that could occur for example when comparing identical points
// (see the NULL note for arccosine in https://sqlite.org/lang_mathfunc.html#overview)
Identifier: `(6371 * acos(min(1, max(-1, ` +
`cos(radians(` + latA + `)) * cos(radians(` + latB + `)) * ` +
`cos(radians(` + lonB + `) - radians(` + lonA + `)) + ` +
`sin(radians(` + latA + `)) * sin(radians(` + latB + `))` +
`))`,
`))))`,
Params: mergeParams(resolvedArgs[0].Params, resolvedArgs[1].Params, resolvedArgs[2].Params, resolvedArgs[3].Params),
}, nil
},
+55 -25
View File
@@ -117,7 +117,7 @@ func TestTokenFunctionsGeoDistance(t *testing.T) {
baseTokenResolver,
&ResolverResult{
NullFallback: NullFallbackDisabled,
Identifier: `(6371 * acos(cos(radians({:latA})) * cos(radians({:latB})) * cos(radians({:lonB}) - radians({:lonA})) + sin(radians({:latA})) * sin(radians({:latB}))))`,
Identifier: `(6371 * acos(min(1, max(-1, cos(radians({:latA})) * cos(radians({:latB})) * cos(radians({:lonB}) - radians({:lonA})) + sin(radians({:latA})) * sin(radians({:latB}))))))`,
Params: map[string]any{
"lonA": 1,
"latA": 2,
@@ -138,7 +138,7 @@ func TestTokenFunctionsGeoDistance(t *testing.T) {
baseTokenResolver,
&ResolverResult{
NullFallback: NullFallbackDisabled,
Identifier: `(6371 * acos(cos(radians({:latA})) * cos(radians({:latB})) * cos(radians({:lonB}) - radians({:lonA})) + sin(radians({:latA})) * sin(radians({:latB}))))`,
Identifier: `(6371 * acos(min(1, max(-1, cos(radians({:latA})) * cos(radians({:latB})) * cos(radians({:lonB}) - radians({:lonA})) + sin(radians({:latA})) * sin(radians({:latB}))))))`,
Params: map[string]any{
"lonA": "null",
"latA": 2,
@@ -178,34 +178,64 @@ func TestTokenFunctionsGeoDistanceExec(t *testing.T) {
t.Error("Expected geoDistance token function to be registered.")
}
result, err := fn(
func(t fexpr.Token) (*ResolverResult, error) {
placeholder := "t" + security.PseudorandomString(5)
return &ResolverResult{Identifier: "{:" + placeholder + "}", Params: map[string]any{placeholder: t.Literal}}, nil
scenarios := []struct {
name string
coords []string // [lonA, latA, lonB, latB]
expected string
}{
{
"distinct points",
[]string{
"23.23033854945808",
"42.713146090563384",
"23.44920680886216",
"42.7078484153991",
},
"17.89",
},
{
"identical points (rounding check with 8 lat)",
[]string{"0", "8", "0", "8"}, // 8 is an example latitude where rounding happens to result in > 1.0
"0.00",
},
{
"identical points (rounding check with 45 lat)",
[]string{"0", "45", "0", "45"}, // 45 is an example latitude where rounding happens to result in > 1.0
"0.00",
},
fexpr.Token{Literal: "23.23033854945808", Type: fexpr.TokenNumber},
fexpr.Token{Literal: "42.713146090563384", Type: fexpr.TokenNumber},
fexpr.Token{Literal: "23.44920680886216", Type: fexpr.TokenNumber},
fexpr.Token{Literal: "42.7078484153991", Type: fexpr.TokenNumber},
)
if err != nil {
t.Fatal(err)
}
column := []float64{}
err = testDB.NewQuery("select " + result.Identifier).Bind(result.Params).Column(&column)
if err != nil {
t.Fatal(err)
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result, err := fn(
func(t fexpr.Token) (*ResolverResult, error) {
placeholder := "t" + security.PseudorandomString(5)
return &ResolverResult{Identifier: "{:" + placeholder + "}", Params: map[string]any{placeholder: t.Literal}}, nil
},
fexpr.Token{Literal: s.coords[0], Type: fexpr.TokenNumber},
fexpr.Token{Literal: s.coords[1], Type: fexpr.TokenNumber},
fexpr.Token{Literal: s.coords[2], Type: fexpr.TokenNumber},
fexpr.Token{Literal: s.coords[3], Type: fexpr.TokenNumber},
)
if err != nil {
t.Fatal(err)
}
if len(column) != 1 {
t.Fatalf("Expected exactly 1 column value as result, got %v", column)
}
column := []float64{}
err = testDB.NewQuery("select " + result.Identifier).Bind(result.Params).Column(&column)
if err != nil {
t.Fatal(err)
}
expected := "17.89"
distance := fmt.Sprintf("%.2f", column[0])
if distance != expected {
t.Fatalf("Expected distance value %s, got %s", expected, distance)
if len(column) != 1 {
t.Fatalf("Expected exactly 1 column value as result, got %v", column)
}
distance := fmt.Sprintf("%.2f", column[0])
if distance != s.expected {
t.Fatalf("Expected distance value %s, got %s", s.expected, distance)
}
})
}
}
+8 -3
View File
@@ -2,6 +2,7 @@ package types
import (
"database/sql/driver"
"encoding/json/jsontext"
"encoding/json/v2"
"fmt"
)
@@ -14,9 +15,13 @@ type jsonArrayAlias[T any] JSONArray[T]
// MarshalJSON implements the [json.Marshaler] interface.
func (m JSONArray[T]) MarshalJSON() ([]byte, error) {
// note: forces the Deterministic option to ensure consistent output
// in mixed json v1 and v2 configurations
return json.Marshal(jsonArrayAlias[T](m), json.Deterministic(true))
// note: forces the Deterministic and AllowInvalidUTF8 options to
// ensure consistent output in mixed json v1 and v2 configurations
return json.Marshal(
jsonArrayAlias[T](m),
json.Deterministic(true),
jsontext.AllowInvalidUTF8(true),
)
}
// String returns the string representation of the current json array.
+1 -1
View File
@@ -18,7 +18,7 @@ func TestJSONArrayMarshalJSON(t *testing.T) {
{types.JSONArray[any]{}, `[]`},
{types.JSONArray[int]{1, 2, 3}, `[1,2,3]`},
{types.JSONArray[string]{"test1", "test2", "test3"}, `["test1","test2","test3"]`},
{types.JSONArray[any]{1, "test"}, `[1,"test"]`},
{types.JSONArray[any]{1, "test\xc3" /* invalid utf8 char to test mangling */}, `[1,"test�"]`},
}
for i, s := range scenarios {
+8 -3
View File
@@ -2,6 +2,7 @@ package types
import (
"database/sql/driver"
"encoding/json/jsontext"
"encoding/json/v2"
"fmt"
)
@@ -29,9 +30,13 @@ func (m JSONMap[T]) Set(key string, value T) {
func (m JSONMap[T]) MarshalJSON() ([]byte, error) {
type alias JSONMap[T] // prevent recursion
// note: forces the Deterministic option to ensure consistent output
// in mixed json v1 and v2 configurations
return json.Marshal(alias(m), json.Deterministic(true))
// note: forces the Deterministic and AllowInvalidUTF8 options to
// ensure consistent output in mixed json v1 and v2 configurations
return json.Marshal(
alias(m),
json.Deterministic(true),
jsontext.AllowInvalidUTF8(true),
)
}
// String returns the string representation of the current json map.
+4 -1
View File
@@ -16,8 +16,11 @@ func TestJSONMapMarshalJSON(t *testing.T) {
}{
{nil, "{}"},
{types.JSONMap[any]{}, `{}`},
{types.JSONMap[any]{"test1": 123, "test2": "lorem"}, `{"test1":123,"test2":"lorem"}`},
{types.JSONMap[any]{"test": []int{1, 2, 3}}, `{"test":[1,2,3]}`},
{
types.JSONMap[any]{"test1": 123, "test2": "lorem\xc3" /* invalid utf8 char to test mangling */},
`{"test1":123,"test2":"lorem�"}`,
},
}
for i, s := range scenarios {
+4 -1
View File
@@ -2,10 +2,13 @@ package types
import (
"database/sql/driver"
"encoding/json/jsontext"
"encoding/json/v2"
"errors"
)
// @todo consider deprecating in favour of jsontext.Value
// JSONRaw defines a json value type that is safe for db read/write.
type JSONRaw []byte
@@ -73,7 +76,7 @@ func (j *JSONRaw) Scan(value any) error {
data = []byte(v)
}
default:
bytes, err := json.Marshal(v, json.Deterministic(true))
bytes, err := json.Marshal(v, json.Deterministic(true), jsontext.AllowInvalidUTF8(true))
if err != nil {
return err
}
+5 -1
View File
@@ -167,7 +167,11 @@ func TestJSONRawScan(t *testing.T) {
{`{"test":1}`, false, `{"test":1}`},
{[]byte(`[1,2,3]`), false, `[1,2,3]`},
{[]int{1, 2, 3}, false, `[1,2,3]`},
{map[string]int{"test": 1}, false, `{"test":1}`},
{
map[string]string{"test": "a\xc3" /* invalid utf8 char to test mangling */},
false,
`{"test":"a�"}`,
},
}
for i, s := range scenarios {
+1 -1
View File
@@ -12,4 +12,4 @@ PB_DOCS_URL = "https://pocketbase.io/docs"
PB_JS_SDK_URL = "https://github.com/pocketbase/js-sdk"
PB_DART_SDK_URL = "https://github.com/pocketbase/dart-sdk"
PB_RELEASES = "https://github.com/pocketbase/pocketbase/releases"
PB_VERSION = "v0.40.0"
PB_VERSION = "v0.40.3"
-1
View File
@@ -1 +0,0 @@
@font-face{font-display:block;font-family:remixicon;src:url(../fonts/remixicon/remixicon.woff2?t=1734404658139)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:normal;font-weight:400;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-regular.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:italic;font-weight:400;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-italic.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:normal;font-weight:600;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-600.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:italic;font-weight:600;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-600italic.woff2)format("woff2")}@font-face{font-display:swap;font-family:IBM Plex Mono;font-style:normal;font-weight:400;src:url(../fonts/ibm-plex-mono/ibm-plex-mono-v20-latin-regular.woff2)format("woff2")}@font-face{font-display:swap;font-family:IBM Plex Mono;font-style:normal;font-weight:600;src:url(../fonts/ibm-plex-mono/ibm-plex-mono-v20-latin-600.woff2)format("woff2")}.ri-24-hours-fill:before{content:""}.ri-24-hours-line:before{content:""}.ri-4k-fill:before{content:""}.ri-4k-line:before{content:""}.ri-a-b:before{content:""}.ri-account-box-fill:before{content:""}.ri-account-box-line:before{content:""}.ri-account-circle-fill:before{content:""}.ri-account-circle-line:before{content:""}.ri-account-pin-box-fill:before{content:""}.ri-account-pin-box-line:before{content:""}.ri-account-pin-circle-fill:before{content:""}.ri-account-pin-circle-line:before{content:""}.ri-add-box-fill:before{content:""}.ri-add-box-line:before{content:""}.ri-add-circle-fill:before{content:""}.ri-add-circle-line:before{content:""}.ri-add-fill:before{content:""}.ri-add-line:before{content:""}.ri-admin-fill:before{content:""}.ri-admin-line:before{content:""}.ri-advertisement-fill:before{content:""}.ri-advertisement-line:before{content:""}.ri-airplay-fill:before{content:""}.ri-airplay-line:before{content:""}.ri-alarm-fill:before{content:""}.ri-alarm-line:before{content:""}.ri-alarm-warning-fill:before{content:""}.ri-alarm-warning-line:before{content:""}.ri-album-fill:before{content:""}.ri-album-line:before{content:""}.ri-alert-fill:before{content:""}.ri-alert-line:before{content:""}.ri-aliens-fill:before{content:""}.ri-aliens-line:before{content:""}.ri-align-bottom:before{content:""}.ri-align-center:before{content:""}.ri-align-justify:before{content:""}.ri-align-left:before{content:""}.ri-align-right:before{content:""}.ri-align-top:before{content:""}.ri-align-vertically:before{content:""}.ri-alipay-fill:before{content:""}.ri-alipay-line:before{content:""}.ri-amazon-fill:before{content:""}.ri-amazon-line:before{content:""}.ri-anchor-fill:before{content:""}.ri-anchor-line:before{content:""}.ri-ancient-gate-fill:before{content:""}.ri-ancient-gate-line:before{content:""}.ri-ancient-pavilion-fill:before{content:""}.ri-ancient-pavilion-line:before{content:""}.ri-android-fill:before{content:""}.ri-android-line:before{content:""}.ri-angularjs-fill:before{content:""}.ri-angularjs-line:before{content:""}.ri-anticlockwise-2-fill:before{content:""}.ri-anticlockwise-2-line:before{content:""}.ri-anticlockwise-fill:before{content:""}.ri-anticlockwise-line:before{content:""}.ri-app-store-fill:before{content:""}.ri-app-store-line:before{content:""}.ri-apple-fill:before{content:""}.ri-apple-line:before{content:""}.ri-apps-2-fill:before{content:""}.ri-apps-2-line:before{content:""}.ri-apps-fill:before{content:""}.ri-apps-line:before{content:""}.ri-archive-drawer-fill:before{content:""}.ri-archive-drawer-line:before{content:""}.ri-archive-fill:before{content:""}.ri-archive-line:before{content:""}.ri-arrow-down-circle-fill:before{content:""}.ri-arrow-down-circle-line:before{content:""}.ri-arrow-down-fill:before{content:""}.ri-arrow-down-line:before{content:""}.ri-arrow-down-s-fill:before{content:""}.ri-arrow-down-s-line:before{content:""}.ri-arrow-drop-down-fill:before{content:""}.ri-arrow-drop-down-line:before{content:""}.ri-arrow-drop-left-fill:before{content:""}.ri-arrow-drop-left-line:before{content:""}.ri-arrow-drop-right-fill:before{content:""}.ri-arrow-drop-right-line:before{content:""}.ri-arrow-drop-up-fill:before{content:""}.ri-arrow-drop-up-line:before{content:""}.ri-arrow-go-back-fill:before{content:""}.ri-arrow-go-back-line:before{content:""}.ri-arrow-go-forward-fill:before{content:""}.ri-arrow-go-forward-line:before{content:""}.ri-arrow-left-circle-fill:before{content:""}.ri-arrow-left-circle-line:before{content:""}.ri-aLine truncated
@@ -1,11 +1,11 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./docsList-DsIHfo2q.js","./expandInfo-CJ9aeAaN.js","./fieldsInfo-CiLcXgNq.js","./docsView-oyuvqZP8.js","./docsCreate-Ap21j9FE.js","./docsUpdate-DAnNYWFv.js","./docsListAuthMethods-CYETXzX7.js","./docsAuthWithPassword-NXiq7pZI.js","./docsAuthWithOAuth2-qI6si9uJ.js","./docsAuthWithOTP-75iz1Xmg.js","./docsAuthRefresh-Cq3xTY6M.js"])))=>i.map(i=>d[i]);
var e=Object.create,n=Object.defineProperty,r=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getPrototypeOf,o=Object.prototype.hasOwnProperty,s=(e,n)=>()=>(n||(e((n={exports:{}}).exports,n),e=null),n.exports),c=(e,a,s,c)=>{if(a&&typeof a==`object`||typeof a==`function`)for(var l=i(a),u=0,d=l.length,f;u<d;u++)f=l[u],!o.call(e,f)&&f!==s&&n(e,f,{get:(e=>a[e]).bind(null,f),enumerable:!(c=r(a,f))||c.enumerable});return e},l=(r,i,s)=>(s=r==null?{}:e(a(r)),c(i||!r||!r.__esModule||!o.call(r,`default`)?n(s,`default`,{value:r,enumerable:!0}):s,r));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))r(e);new MutationObserver(e=>{for(let n of e)if(n.type===`childList`)for(let e of n.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&r(e)}).observe(document,{childList:!0,subtree:!0});function n(e){let n={};return e.integrity&&(n.integrity=e.integrity),e.referrerPolicy&&(n.referrerPolicy=e.referrerPolicy),n.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,n}function r(e){if(e.ep)return;e.ep=!0;let r=n(e);fetch(e.href,r)}})();var u=`abcdefghijklmnopqrstuvwxyz0123456789`,d=store({hash:window.location.hash});window.addEventListener(`hashchange`,()=>{d.hash=window.location.hash}),Prism.languages.pbrule={string:Prism.languages.js.string,number:Prism.languages.js.number,function:Prism.languages.js.function,boolean:/\b(?:true|false)\b/i,constant:/\b(?:null)\b/i,comment:{pattern:/\/\/.*/,greedy:!0},italic:/_via_|\:\w+/,keyword:/&&|\|\||\??(?:!~|!=|>=|<=|=|~|>|>|<)(?=[@\w\s]|$)/};var f={isObject(e){return typeof e==`object`&&!!e&&e.constructor===Object},isEmpty(e){return e==null||e===``||Array.isArray(e)&&e.length===0||typeof e==`object`&&app.utils.isEmptyObject(e)},isEmptyObject(e){for(let n in e)return!1;return!0},toArray(e,n=!1){return Array.isArray(e)?e.slice():(n||!f.isEmpty(e))&&e!==void 0?[e]:[]},removeByValue(e,n){if(!Array.isArray(e)){console.warn(`[removeByValue] not an array:`,e);return}for(let r=e.length-1;r>=0;r--)if(e[r]==n){e.splice(r,1);break}},removeByKey(e,n,r){if(!Array.isArray(e)){console.warn(`[removeByKey] not an array:`,e);return}for(let i in e)if(e[i][n]==r){e.splice(i,1);break}},pushUnique(e,n){if(!Array.isArray(e)){console.warn(`[pushUnique] not an array:`,e);return}e.includes(n)||e.push(n)},mergeUnique(e,n){for(let r of n)app.utils.pushUnique(e,r);return e},pushOrReplaceObject(e,n,r=`id`){for(let i=e.length-1;i>=0;i--)if(e[i][r]==n[r]){e[i]=n;return}e.push(n)},filterDuplicatesByKey(e,n=`id`){e=Array.isArray(e)?e:[];let r={};for(let i of e)r[i[n]]=i;return Object.values(r)},filterRedactedProps(e,n=`******`){let r=JSON.parse(JSON.stringify(e||{}));for(let e in r)typeof r[e]==`object`&&r[e]!==null?r[e]=f.filterRedactedProps(r[e],n):r[e]===n&&delete r[e];return r},getByPath(e,n,r=null,i=`.`){let a=e||{},o=(n||``).split(i);for(let e of o){if(!f.isObject(a)&&!Array.isArray(a)||a[e]===void 0)return r;a=a[e]}return a},setByPath(e,n,r,i=`.`){if(typeof e!=`object`||!e){console.warn(`setByPath: data not an object or array.`);return}let a=e,o=n.split(i),s=o.pop();for(let e of o)(!f.isObject(a)&&!Array.isArray(a)||!f.isObject(a[e])&&!Array.isArray(a[e]))&&(a[e]={}),a=a[e];a[s]=r},deleteByPath(e,n,r=`.`){let i=e||{},a=(n||``).split(r),o=a.pop();for(let e of a)(!f.isObject(i)&&!Array.isArray(i)||!f.isObject(i[e])&&!Array.isArray(i[e]))&&(i[e]={}),i=i[e];Array.isArray(i)?i.splice(o,1):f.isObject(i)&&delete i[o],a.length>0&&(Array.isArray(i)&&!i.length||f.isObject(i)&&!Object.keys(i).length)&&(Array.isArray(e)&&e.length>0||f.isObject(e)&&Object.keys(e).length>0)&&f.deleteByPath(e,a.join(r),r)},emptyClone(e,n=[]){let r=JSON.parse(JSON.stringify(e));for(let e in r)n.includes(e)||(typeof r[e]==`string`?r[e]=``:typeof r[e]==`number`?r[e]=0:typeof r[e]==`boolean`?r[e]=!1:Array.isArray(r[e])?r[e]=[]:app.utils.isObject(r[e])&&(r[e]={}));return r},randomString(e=8,n=u){let r=``;for(let i=0;i<e;i++)r+=n.charAt(Math.floor(Math.random()*n.length));return r},randomSecret(e=30){if(typeof crypto>`u`)return app.utils.randomString(e);let n=new Uint8Array(e);crypto.getRandomValues(n);let r=``;for(let i=0;i<e;i++)r+=`-_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`.charAt(n[i]%64);return r},sentenize(e,n=!0){if(typeof e!=`string`)return``;if(e=e.trim().split(`_`).join(` `),e===``)return e;if(e=e[0].toUpperCase()+e.substring(1),n){let n=e[e.length-1];n!==`.`&&n!==`?`&&n!==`!`&&(e+=`.`)}return e},trimQuotedValue(e){return typeof e==`string`&&(e[0]==`"`||e[0]==`'`||e[0]=="`")&&e[0]==e[e.length-1]?e.slice(1,-1):e},slugify(e,n=`_`,r=[]){if(e===``)return``;let i={a:/а|à|á|å|â/gi,b:/б/gi,c:/ц|ç/gi,d:/д/gi,e:/е|è|é|ê|ẽ|ë/gi,f:/ф/gi,g:/г/gi,h:/х/gi,i:/й|и|ì|í|î/gi,j:/ж/gi,k:/к/gi,l:/л/gi,m:/м/gi,n:/н|ñ/gi,o:/о|ò|ó|ô|ø/gi,p:/п/gi,q:/я/gi,r:/р/gi,s:/с/gi,t:/т/gi,u:/ю|ù|ú|ů|û/gi,v:Line truncated
`),i=new Blob([r],{type:`text/csv;charset=utf-8;`}),a=window.URL.createObjectURL(i);f.download(a,n)},getApiExampleURL(){let e;if(app.pb.baseURL.startsWith(`http://`)||app.pb.baseURL.startsWith(`https://`))e=app.pb.baseURL;else{e=window.location.href;let n=e.indexOf(`/_/`);e=n>=0?e.substring(0,n):window.location.origin}return e.replace(`//localhost`,`//127.0.0.1`)},isActivePath(e,n=!0,r=``){r||=d.hash;let i;return i=RegExp(n?`^`+RegExp.escape(e)+`\\/?.*$`:`^`+RegExp.escape(e)+`\\/?(?:\\?.+)?$`),i.test(r)},getHashQueryParams(e=``){e||=d.hash;let n=``,r=e.indexOf(`?`);return r>-1&&(n=window.location.hash.substring(r+1)),Object.fromEntries(new URLSearchParams(n))},replaceHashQueryParams(e,n=null){e||={};let r=``,i=window.location.hash,a=i.indexOf(`?`);a>-1&&(r=i.substring(a+1),i=i.substring(0,a));let o=new URLSearchParams(r);for(let n in e){let r=e[n];f.isEmpty(r)?o.delete(n):o.set(n,r)}r=o.toString(),r!=``&&(i+=`?`+r);let s=window.location.href,c=s.indexOf(`#`);c>-1&&(s=s.substring(0,c));let l=s+i;return n===!1||(n===!0?window.history.pushState(null,``,l):window.history.replaceState(null,``,l)),l},getLocalHistory(e,n=null){try{let r=window.localStorage.getItem(e);if(r)return JSON.parse(r)||n}catch(n){console.log(`failed to load local history:`,e,n)}return n},saveLocalHistory(e,n){try{app.utils.isEmpty(n)?window.localStorage.removeItem(e):typeof n==`string`?window.localStorage.setItem(e,n):window.localStorage.setItem(e,JSON.stringify(n))}catch(n){console.log(`failed to save local history:`,e,n)}},generateThumb(e,n=100,r=100){return new Promise(i=>{let a=new FileReader;a.onload=function(a){let o=new Image;o.onload=function(){let a=document.createElement(`canvas`),s=a.getContext(`2d`),c=o.width,l=o.height;return a.width=n,a.height=r,s.drawImage(o,c>l?(c-l)/2:0,0,c>l?l:c,c>l?l:c,0,0,n,r),i(a.toDataURL(e.type))},o.src=a.target.result},a.readAsDataURL(e)})},normalizeSearchFilter(e,n=[]){if(e=(e||``).trim(),!e||!n.length)return e;for(let n of[`=`,`~`,`>`,`<`])if(e.includes(n))return e;return e=isNaN(e)&&e!=`true`&&e!=`false`?e.replace(/^[\"\'\`]|[\"\'\`]$/gm,``):e,n.map(n=>app.pb.filter(`${n}?~{:searchTerm}`,{searchTerm:e})).join(`||`)},logLevels:{[-4]:{label:`DEBUG`,class:``},0:{label:`INFO`,class:`success`},4:{label:`WARN`,class:`warning`},8:{label:`ERROR`,class:`danger`}},logDataFormatters:{execTime:function(e){return e?.data?.execTime===void 0?`N/A`:e.data.execTime+`ms`}},extendStore(e,n={},...r){let i=[];for(let a in n){let o=n[a];typeof e.__raw?.[a]==`function`||typeof o!=`function`||a.length>2&&a.startsWith(`on`)||r.includes(a)?e[a]=o:i.push(watch(o,n=>{e[a]=n}))}return i},cssTimeToMs(e){return e?(e=e.toLowerCase(),e.endsWith(`ms`)?Number(e.substring(0,e.length-2)):e.endsWith(`s`)?Number(e.substring(0,e.length-1)):Number(e)||0):0},isDarkEnoughForWhiteText(e){if(e=e?.startsWith(`#`)?e.substring(1):e,e?.length!=6)return!1;let n=parseInt(e.substring(0,2),16),r=parseInt(e.substring(2,4),16),i=parseInt(e.substring(4,6),16);return(n*299+r*587+i*114)/1e3<128},bulkSelectRange(e,n,r,i){let a=e.findIndex(e=>e.id==r.id);if(a==-1)return;let o=Object.keys(n),s=o[o.length-1],c=e.findIndex(e=>e.id==s);if(c==-1)return;let l=c,u=a;l>u&&([l,u]=[u,l]);for(let r=l;r<=u;r++)i?n[e[r].id]=e[r]:delete n[e[r].id]},imageExtensions:[`.jpg`,`.jpeg`,`.png`,`.svg`,`.gif`,`.jfif`,`.webp`,`.avif`],videoExtensions:[`.mp4`,`.avi`,`.mov`,`.3gp`,`.wmv`],audioExtensions:[`.aa`,`.aac`,`.m4v`,`.mp3`,`.ogg`,`.oga`,`.mogg`,`.amr`],documentExtensions:[`.pdf`,`.doc`,`.docx`,`.xls`,`.xlsx`,`.ppt`,`.pptx`,`.odp`,`.odt`,`.ods`,`.txt`],archiveExtensions:[`.zip`,`.gz`,`.tar`,`.rar`,`.7z`],hasExtension(e,n){return typeof n==`string`&&(n=n.toLowerCase(),!!e?.find(e=>n.endsWith(e)))},hasImageExtension(e){return app.utils.hasExtension(app.utils.imageExtensions,e)},hasVideoExtension(e){return app.utils.hasExtension(app.utils.videoExtensions,e)},hasAudioExtension(e){return app.utils.hasExtension(app.utils.audioExtensions,e)},hasDocumentExtension(e){return app.utils.hasExtension(app.utils.documentExtensions,e)},hasArchiveExtension(e){return app.utils.hasExtension(app.utils.archiveExtensions,e)},getFileType(e){return app.utils.hasImageExtension(e)?`image`:app.utils.hasVideoExtension(e)?`video`:app.utils.hasAudioExtension(e)?`audio`:app.utils.hasDocumentExtension(e)?`document`:app.utils.hasArchiveExtension(e)?`archive`:`file`},fileTypeIcons:{image:`ri-image-line`,video:`ri-movie-line`,audio:`ri-music-2-line`,document:`ri-file-line`,archive:`ri-file-zip-line`,file:`ri-file-line`},fallbackFieldIcon:`ri-puzzle-line`,fallbackCollectionIcon:`ri-puzzle-line`,fallbackProviderIcon:`ri-puzzle-line`,fallbackPresentableProps:[`title`,`name`,`slug`,`email`,`username`,`nickname`,`displayName`,`label`,`subject`,`topic`,`message`,`heading`,`headline`,`header`,`caption`,`key`,`identifier`,`id`],sortedCollections(e=[]){let n,r;function i(e,i){return n=e.name.startsWith(`_`),r=i.name.startsWith(`_`),n&&!r?1:!n&&r?-1:e.name>i.name?1:e.name<i.name?-1:0}return e.slice().sort(i)},sorted Line truncated
`),i=new Blob([r],{type:`text/csv;charset=utf-8;`}),a=window.URL.createObjectURL(i);f.download(a,n)},getApiExampleURL(){let e;if(app.pb.baseURL.startsWith(`http://`)||app.pb.baseURL.startsWith(`https://`))e=app.pb.baseURL;else{e=window.location.href;let n=e.indexOf(`/_/`);e=n>=0?e.substring(0,n):window.location.origin}return e.replace(`//localhost`,`//127.0.0.1`)},isActivePath(e,n=!0,r=``){r||=d.hash;let i;return i=RegExp(n?`^`+RegExp.escape(e)+`\\/?.*$`:`^`+RegExp.escape(e)+`\\/?(?:\\?.+)?$`),i.test(r)},getHashQueryParams(e=``){e||=d.hash;let n=``,r=e.indexOf(`?`);return r>-1&&(n=window.location.hash.substring(r+1)),Object.fromEntries(new URLSearchParams(n))},replaceHashQueryParams(e,n=null){e||={};let r=``,i=window.location.hash,a=i.indexOf(`?`);a>-1&&(r=i.substring(a+1),i=i.substring(0,a));let o=new URLSearchParams(r);for(let n in e){let r=e[n];f.isEmpty(r)?o.delete(n):o.set(n,r)}r=o.toString(),r!=``&&(i+=`?`+r);let s=window.location.href,c=s.indexOf(`#`);c>-1&&(s=s.substring(0,c));let l=s+i;return n===!1||(n===!0?window.history.pushState(null,``,l):window.history.replaceState(null,``,l)),l},getLocalHistory(e,n=null){try{let r=window.localStorage.getItem(e);if(r)return JSON.parse(r)||n}catch(n){console.log(`failed to load local history:`,e,n)}return n},saveLocalHistory(e,n){try{app.utils.isEmpty(n)?window.localStorage.removeItem(e):typeof n==`string`?window.localStorage.setItem(e,n):window.localStorage.setItem(e,JSON.stringify(n))}catch(n){console.log(`failed to save local history:`,e,n)}},generateThumb(e,n=100,r=100){return new Promise(i=>{let a=new FileReader;a.onload=function(a){let o=new Image;o.onload=function(){let a=document.createElement(`canvas`),s=a.getContext(`2d`),c=o.width,l=o.height;return a.width=n,a.height=r,s.drawImage(o,c>l?(c-l)/2:0,0,c>l?l:c,c>l?l:c,0,0,n,r),i(a.toDataURL(e.type))},o.src=a.target.result},a.readAsDataURL(e)})},normalizeSearchFilter(e,n=[]){if(e=(e||``).trim(),!e||!n.length)return e;for(let n of[`=`,`~`,`>`,`<`])if(e.includes(n))return e;return e=isNaN(e)&&e!=`true`&&e!=`false`?e.replace(/^[\"\'\`]|[\"\'\`]$/gm,``):e,n.map(n=>app.pb.filter(`${n}?~{:searchTerm}`,{searchTerm:e})).join(`||`)},logLevels:{[-4]:{label:`DEBUG`,class:``},0:{label:`INFO`,class:`success`},4:{label:`WARN`,class:`warning`},8:{label:`ERROR`,class:`danger`}},logDataFormatters:{execTime:function(e){return e?.data?.execTime===void 0?`N/A`:e.data.execTime+`ms`}},extendStore(e,n={},...r){let i=[];for(let a in n){let o=n[a];typeof e.__raw?.[a]==`function`||typeof o!=`function`||a.length>2&&a.startsWith(`on`)||r.includes(a)?e[a]=o:i.push(watch(o,n=>{e[a]=n}))}return i},cssTimeToMs(e){return e?(e=e.toLowerCase(),e.endsWith(`ms`)?Number(e.substring(0,e.length-2)):e.endsWith(`s`)?Number(e.substring(0,e.length-1)):Number(e)||0):0},isDarkEnoughForWhiteText(e){if(e=e?.startsWith(`#`)?e.substring(1):e,e?.length!=6)return!1;let n=parseInt(e.substring(0,2),16),r=parseInt(e.substring(2,4),16),i=parseInt(e.substring(4,6),16);return(n*299+r*587+i*114)/1e3<128},bulkSelectRange(e,n,r,i){let a=e.findIndex(e=>e.id==r.id);if(a==-1)return;let o=Object.keys(n),s=o[o.length-1],c=e.findIndex(e=>e.id==s);if(c==-1)return;let l=c,u=a;l>u&&([l,u]=[u,l]);for(let r=l;r<=u;r++)i?n[e[r].id]=e[r]:delete n[e[r].id]},imageExtensions:[`.jpg`,`.jpeg`,`.png`,`.svg`,`.gif`,`.jfif`,`.webp`,`.avif`],videoExtensions:[`.mp4`,`.avi`,`.mov`,`.3gp`,`.wmv`],audioExtensions:[`.aa`,`.aac`,`.m4v`,`.mp3`,`.ogg`,`.oga`,`.mogg`,`.amr`],documentExtensions:[`.pdf`,`.doc`,`.docx`,`.xls`,`.xlsx`,`.ppt`,`.pptx`,`.odp`,`.odt`,`.ods`,`.txt`],archiveExtensions:[`.zip`,`.gz`,`.tar`,`.rar`,`.7z`],hasExtension(e,n){return typeof n==`string`&&(n=n.toLowerCase(),!!e?.find(e=>n.endsWith(e)))},hasImageExtension(e){return app.utils.hasExtension(app.utils.imageExtensions,e)},hasVideoExtension(e){return app.utils.hasExtension(app.utils.videoExtensions,e)},hasAudioExtension(e){return app.utils.hasExtension(app.utils.audioExtensions,e)},hasDocumentExtension(e){return app.utils.hasExtension(app.utils.documentExtensions,e)},hasArchiveExtension(e){return app.utils.hasExtension(app.utils.archiveExtensions,e)},getFileType(e){return app.utils.hasImageExtension(e)?`image`:app.utils.hasVideoExtension(e)?`video`:app.utils.hasAudioExtension(e)?`audio`:app.utils.hasDocumentExtension(e)?`document`:app.utils.hasArchiveExtension(e)?`archive`:`file`},fileTypeIcons:{image:`ri-image-line`,video:`ri-movie-line`,audio:`ri-music-2-line`,document:`ri-file-line`,archive:`ri-file-zip-line`,file:`ri-file-line`},fallbackFieldIcon:`ri-puzzle-line`,fallbackCollectionIcon:`ri-puzzle-line`,fallbackProviderIcon:`ri-puzzle-line`,fallbackPresentableProps:[`title`,`name`,`slug`,`email`,`username`,`nickname`,`displayName`,`label`,`subject`,`topic`,`message`,`heading`,`headline`,`header`,`caption`,`key`,`identifier`,`id`],sortedCollections(e=[]){let n,r;function i(e,i){return n=e.name.startsWith(`_`),r=i.name.startsWith(`_`),n&&!r?1:!n&&r?-1:e.name>i.name?1:e.name<i.name?-1:0}return e.slice().sort(i)},sorted Line truncated
`),n+=r.map(e=>{let n=``;return e.name.includes(`(`)||e.name.includes(` `)?n+=e.name:n+="`"+e.name+"`",e.collate&&(n+=` COLLATE `+e.collate),e.sort&&(n+=` `+e.sort.toUpperCase()),n}).join(`,
`),r.length>1&&(n+=`
`),n+=`)`,e.where&&(n+=` WHERE ${e.where}`),n},replaceIndexFields(e,n){let r=app.utils.parseIndex(e);return typeof n==`function`?Object.assign(r,n(r)||{}):Object.assign(r,n||{}),app.utils.buildIndex(r)},replaceIndexColumn(e,n,r){if(n===r)return e;let i=app.utils.parseIndex(e),a=!1;for(let e of i.columns)e.name===n&&(e.name=r,a=!0);return a?app.utils.buildIndex(i):e}};window.app=window.app||{},window.app.utils=f,window.app=window.app||{},window.app.utils=window.app.utils||{},window.app.utils.mimeTypes=[{ext:``,mimeType:`application/octet-stream`},{ext:`.xpm`,mimeType:`image/x-xpixmap`},{ext:`.7z`,mimeType:`application/x-7z-compressed`},{ext:`.zip`,mimeType:`application/zip`},{ext:`.xlsx`,mimeType:`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`},{ext:`.docx`,mimeType:`application/vnd.openxmlformats-officedocument.wordprocessingml.document`},{ext:`.pptx`,mimeType:`application/vnd.openxmlformats-officedocument.presentationml.presentation`},{ext:`.epub`,mimeType:`application/epub+zip`},{ext:`.apk`,mimeType:`application/vnd.android.package-archive`},{ext:`.jar`,mimeType:`application/jar`},{ext:`.odt`,mimeType:`application/vnd.oasis.opendocument.text`},{ext:`.ott`,mimeType:`application/vnd.oasis.opendocument.text-template`},{ext:`.ods`,mimeType:`application/vnd.oasis.opendocument.spreadsheet`},{ext:`.ots`,mimeType:`application/vnd.oasis.opendocument.spreadsheet-template`},{ext:`.odp`,mimeType:`application/vnd.oasis.opendocument.presentation`},{ext:`.otp`,mimeType:`application/vnd.oasis.opendocument.presentation-template`},{ext:`.odg`,mimeType:`application/vnd.oasis.opendocument.graphics`},{ext:`.otg`,mimeType:`application/vnd.oasis.opendocument.graphics-template`},{ext:`.odf`,mimeType:`application/vnd.oasis.opendocument.formula`},{ext:`.odc`,mimeType:`application/vnd.oasis.opendocument.chart`},{ext:`.sxc`,mimeType:`application/vnd.sun.xml.calc`},{ext:`.pdf`,mimeType:`application/pdf`},{ext:`.fdf`,mimeType:`application/vnd.fdf`},{ext:``,mimeType:`application/x-ole-storage`},{ext:`.msi`,mimeType:`application/x-ms-installer`},{ext:`.aaf`,mimeType:`application/octet-stream`},{ext:`.msg`,mimeType:`application/vnd.ms-outlook`},{ext:`.xls`,mimeType:`application/vnd.ms-excel`},{ext:`.pub`,mimeType:`application/vnd.ms-publisher`},{ext:`.ppt`,mimeType:`application/vnd.ms-powerpoint`},{ext:`.doc`,mimeType:`application/msword`},{ext:`.ps`,mimeType:`application/postscript`},{ext:`.psd`,mimeType:`image/vnd.adobe.photoshop`},{ext:`.p7s`,mimeType:`application/pkcs7-signature`},{ext:`.ogg`,mimeType:`application/ogg`},{ext:`.oga`,mimeType:`audio/ogg`},{ext:`.ogv`,mimeType:`video/ogg`},{ext:`.png`,mimeType:`image/png`},{ext:`.png`,mimeType:`image/vnd.mozilla.apng`},{ext:`.jpg`,mimeType:`image/jpeg`},{ext:`.jxl`,mimeType:`image/jxl`},{ext:`.jp2`,mimeType:`image/jp2`},{ext:`.jpf`,mimeType:`image/jpx`},{ext:`.jpm`,mimeType:`image/jpm`},{ext:`.jxs`,mimeType:`image/jxs`},{ext:`.gif`,mimeType:`image/gif`},{ext:`.webp`,mimeType:`image/webp`},{ext:`.exe`,mimeType:`application/vnd.microsoft.portable-executable`},{ext:``,mimeType:`application/x-elf`},{ext:``,mimeType:`application/x-object`},{ext:``,mimeType:`application/x-executable`},{ext:`.so`,mimeType:`application/x-sharedlib`},{ext:``,mimeType:`application/x-coredump`},{ext:`.a`,mimeType:`application/x-archive`},{ext:`.deb`,mimeType:`application/vnd.debian.binary-package`},{ext:`.tar`,mimeType:`application/x-tar`},{ext:`.xar`,mimeType:`application/x-xar`},{ext:`.bz2`,mimeType:`application/x-bzip2`},{ext:`.fits`,mimeType:`application/fits`},{ext:`.tiff`,mimeType:`image/tiff`},{ext:`.bmp`,mimeType:`image/bmp`},{ext:`.ico`,mimeType:`image/x-icon`},{ext:`.mp3`,mimeType:`audio/mpeg`},{ext:`.flac`,mimeType:`audio/flac`},{ext:`.midi`,mimeType:`audio/midi`},{ext:`.ape`,mimeType:`audio/ape`},{ext:`.mpc`,mimeType:`audio/musepack`},{ext:`.amr`,mimeType:`audio/amr`},{ext:`.wav`,mimeType:`audio/wav`},{ext:`.aiff`,mimeType:`audio/aiff`},{ext:`.au`,mimeType:`audio/basic`},{ext:`.mpeg`,mimeType:`video/mpeg`},{ext:`.mov`,mimeType:`video/quicktime`},{ext:`.mp4`,mimeType:`video/mp4`},{ext:`.avif`,mimeType:`image/avif`},{ext:`.3gp`,mimeType:`video/3gpp`},{ext:`.3g2`,mimeType:`video/3gpp2`},{ext:`.mp4`,mimeType:`audio/mp4`},{ext:`.mqv`,mimeType:`video/quicktime`},{ext:`.m4a`,mimeType:`audio/x-m4a`},{ext:`.m4v`,mimeType:`video/x-m4v`},{ext:`.heic`,mimeType:`image/heic`},{ext:`.heic`,mimeType:`image/heic-sequence`},{ext:`.heif`,mimeType:`image/heif`},{ext:`.heif`,mimeType:`image/heif-sequence`},{ext:`.mj2`,mimeType:`video/mj2`},{ext:`.dvb`,mimeType:`video/vnd.dvb.file`},{ext:`.webm`,mimeType:`video/webm`},{ext:`.avi`,mimeType:`video/x-msvideo`},{ext:`.flv`,mimeType:`video/x-flv`},{ext:`.mkv`,mimeType:`video/x-matroska`},{ext:`.asf`,mimeType:`video/x-ms-asf`},{ext:`.asf`,mimeType:`video/x-ms-wmv`},{ext:`.asf`,mimeType:`video/asf`},{ext:`.aac`,mimeType:`audio/aac`},{ext:`.voc`,mimeType:`audio/x-unknown`},{ext:`.m3u`,mimeType:`application/vnd.apple.mpegurl`},{ext:`.rmvb`,mimeType:`application/Line truncated
`)}o&&(n.classList.add(`error`),r.__errListener=function(){te(r,n),app.utils.deleteByPath(app.store.errors,i)},r.addEventListener(`input`,r.__errListener),r.addEventListener(`change`,r.__errListener),r.setAttribute(`data-error`,!0),r.setCustomValidity&&r.reportValidity&&r.classList.contains(`inline-error`)?(r.setCustomValidity(o),r.reportValidity(),r._oldTitle=r.title,r.title=o):n.after(t.div({"html-data-input-name":i,className:`field-help error generated-error`,textContent:o})))}}),p.onmessage=e=>{e.data?.collections&&JSON.stringify(app.store.collections)!=JSON.stringify(e.data.collections)&&(app.store.collections=e.data.collections),e.data?.settings&&JSON.stringify(app.store.settings)!=JSON.stringify(e.data.settings)&&(app.store.settings=e.data.settings),e.data?.colorScheme&&(app.store.userColorScheme=e.data.colorScheme)},watch(()=>JSON.stringify(app.store.collections),(e,n)=>{e&&e!=`[]`&&n&&n!=`[]`&&e!=n&&p?.postMessage({collections:JSON.parse(e)})}),watch(()=>JSON.stringify(app.store.settings),(e,n)=>{e&&e!=`{}`&&n&&n!=`{}`&&e!=n&&p?.postMessage({settings:JSON.parse(e)}),window.localStorage.setItem(m,e)});var y=class e extends Error{constructor(n){super(`ClientResponseError`),this.url=``,this.status=0,this.response={},this.isAbort=!1,this.originalError=null,Object.setPrototypeOf(this,e.prototype),typeof n==`object`&&n&&(this.originalError=n.originalError,this.url=typeof n.url==`string`?n.url:``,this.status=typeof n.status==`number`?n.status:0,this.isAbort=!!n.isAbort||n.name===`AbortError`||n.message===`Aborted`,this.response=n.response!==null&&typeof n.response==`object`?n.response:n.data!==null&&typeof n.data==`object`?n.data:{}),this.originalError||n instanceof e||(this.originalError=n),this.name=`ClientResponseError `+this.status,this.message=this.response?.message,this.message||=this.isAbort?`The request was aborted (most likely autocancelled; you can find more info in https://github.com/pocketbase/js-sdk#auto-cancellation).`:this.originalError?.cause?.message?.includes(`ECONNREFUSED ::1`)?`Failed to connect to the PocketBase server. Try changing the SDK URL from localhost to 127.0.0.1 (https://github.com/pocketbase/js-sdk/issues/21).`:`Something went wrong.`,this.cause=this.originalError}get data(){return this.response}toJSON(){return{...this}}},ne=/^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;function re(e,n){let r={};if(typeof e!=`string`)return r;let i=Object.assign({},n||{}).decode||ae,a=0;for(;a<e.length;){let n=e.indexOf(`=`,a);if(n===-1)break;let o=e.indexOf(`;`,a);if(o===-1)o=e.length;else if(o<n){a=e.lastIndexOf(`;`,n-1)+1;continue}let s=e.slice(a,n).trim();if(r[s]===void 0){let a=e.slice(n+1,o).trim();a.charCodeAt(0)===34&&(a=a.slice(1,-1));try{r[s]=i(a)}catch{r[s]=a}}a=o+1}return r}function ie(e,n,r){let i=Object.assign({},r||{}),a=i.encode||b;if(!ne.test(e))throw TypeError(`argument name is invalid`);let o=a(n);if(o&&!ne.test(o))throw TypeError(`argument val is invalid`);let s=e+`=`+o;if(i.maxAge!=null){let e=i.maxAge-0;if(isNaN(e)||!isFinite(e))throw TypeError(`option maxAge is invalid`);s+=`; Max-Age=`+Math.floor(e)}if(i.domain){if(!ne.test(i.domain))throw TypeError(`option domain is invalid`);s+=`; Domain=`+i.domain}if(i.path){if(!ne.test(i.path))throw TypeError(`option path is invalid`);s+=`; Path=`+i.path}if(i.expires){if(!function(e){return Object.prototype.toString.call(e)===`[object Date]`||e instanceof Date}(i.expires)||isNaN(i.expires.valueOf()))throw TypeError(`option expires is invalid`);s+=`; Expires=`+i.expires.toUTCString()}if(i.httpOnly&&(s+=`; HttpOnly`),i.secure&&(s+=`; Secure`),i.priority)switch(typeof i.priority==`string`?i.priority.toLowerCase():i.priority){case`low`:s+=`; Priority=Low`;break;case`medium`:s+=`; Priority=Medium`;break;case`high`:s+=`; Priority=High`;break;default:throw TypeError(`option priority is invalid`)}if(i.sameSite)switch(typeof i.sameSite==`string`?i.sameSite.toLowerCase():i.sameSite){case!0:s+=`; SameSite=Strict`;break;case`lax`:s+=`; SameSite=Lax`;break;case`strict`:s+=`; SameSite=Strict`;break;case`none`:s+=`; SameSite=None`;break;default:throw TypeError(`option sameSite is invalid`)}return s}function ae(e){return e.indexOf(`%`)===-1?e:decodeURIComponent(e)}function b(e){return encodeURIComponent(e)}var x=typeof navigator<`u`&&navigator.product===`ReactNative`||typeof global<`u`&&global.HermesInternal,oe;function S(e){if(e)try{let n=decodeURIComponent(oe(e.split(`.`)[1]).split(``).map((function(e){return`%`+(`00`+e.charCodeAt(0).toString(16)).slice(-2)})).join(``));return JSON.parse(n)||{}}catch{}return{}}function se(e,n=0){let r=S(e);return!(Object.keys(r).length>0&&(!r.exp||r.exp-n>Date.now()/1e3))}oe=typeof atob!=`function`||x?e=>{let n=String(e).replace(/=+$/,``);if(n.length%4==1)throw Error(`'atob' failed: The string to be decoded is not correctly encoded.`);for(var r,i,a=0,o=0,s=``;i=n.charAt(o++);~i&&(r=a%4?64*r+i:i,a++%4)&&(s+=String.fromCharCode(255&r>>(-2*a&6))))i=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=`.in Line truncated
`+ft:null),"html-class":`formatted-date`,className:()=>`formatted-date ${n.short?`short`:`full`}`,onunmount:()=>{r.forEach(e=>e?.unwatch())}},()=>{if(!n.value)return t.span({className:`missing-value`});if(n.short){let e=n.value.split(` `);return[t.span({className:`primary-date`},e[0]),t.span({className:`secondary-date`},e[1])]}return[t.span({className:`primary-date`},app.utils.toLocalDatetime(n.value)),t.span({className:`secondary-date`},n.value)]})},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.refreshButton=function(e={}){let n=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,tooltip:`Refresh`,className:`btn transparent secondary circle rotate-btn`,disabled:!1,onclick:function(e){}}),r=app.utils.extendStore(n,e),i,a=t.button({rid:n.rid,id:()=>n.id,hidden:()=>n.hidden,inert:()=>n.inert,type:`button`,ariaLabel:app.attrs.tooltip(()=>n.tooltip),disabled:()=>n.disabled,className:()=>n.className,onunmount:()=>{clearTimeout(i),r.forEach(e=>e?.unwatch())},onclick:e=>{e.preventDefault(),n.onclick&&n.onclick(e),a.dataset.rotate=!0,a.addEventListener(`animationend`,()=>{a.dataset.rotate=!1}),clearTimeout(i),i=setTimeout(()=>{clearTimeout(i),a.dataset.rotate=!1},500)}},t.i({className:`ri-refresh-line`,ariaHidden:!0}));return a},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.searchHistoryButton=function(e={}){let n=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,value:void 0,historyKey:`default`,max:15,openInNewTabParam:`filter`,btnClassName:`btn sm pill secondary transparent p-r-5`,onselect:function(e){}}),r=app.utils.extendStore(n,e),i=store({items:app.utils.getLocalHistory(n.historyKey,[])});function a(e){o(e),i.items.unshift(e)}function o(e){app.utils.removeByValue(i.items,e)}let s=`history_dropdown_`+app.utils.randomString();r.push(watch(()=>n.value,e=>{e&&a(e)})),r.push(watch(()=>{i.items.length>n.max&&(i.items=i.items.slice(0,n.max)),app.utils.saveLocalHistory(n.historyKey,i.items)}));let c=t.div({id:s,className:`dropdown sm left nowrap history-searchbar-dropdown`,popover:`hint`,onclick:e=>(e.stopPropagation(),!1)},t.div({className:`block p-5`},t.small({className:`txt-hint`},`Search history`)),()=>i.items?.length?i.items.slice(0,n.max).map(e=>t.button({type:`button`,className:`dropdown-item txt-code`,onclick:()=>{c.hidePopover(),n.onselect?.(e),a(e)},onauxclick:()=>{if(n.openInNewTabParam){a(e),c.hidePopover();let r=app.utils.replaceHashQueryParams({[n.openInNewTabParam]:e},!1);window.open(r,`_blank`)}}},t.span({className:`txt-ellipsis`,title:e,textContent:e}),t.small({role:`button`,className:`remove-btn link-hint m-l-auto p-l-5 p-r-5`,title:`Clear`,onauxclick:e=>(e.stopPropagation(),!1),onclick:n=>(n.stopPropagation(),o(e),!1)},t.i({className:`ri-close-line`,ariaHidden:!0})))):t.div({rid:`no-history`,className:`block p-5`},t.span(null,`Your recent searches will show up here.`)));return t.button({rid:n.rid,id:()=>n.id,hidden:()=>n.hidden,inert:()=>n.inert,type:`button`,title:`Search history`,className:()=>n.btnClassName,"html-popovertarget":s,onunmount:()=>{r?.forEach(e=>e?.unwatch())}},t.i({className:`ri-search-line`,ariaHidden:!0}),t.i({className:`ri-arrow-drop-down-line`,ariaHidden:!0}),c)},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.s3Test=function(e={}){let n=`s3_test_request`,r=store({rid:void 0,config:null,label:`Use S3 storage`,testFilesystem:`storage`}),i=app.utils.extendStore(r,e),a=store({isTesting:!1,testError:null,get hasError(){return!app.utils.isEmpty(a.testError)}}),o,s;function c(e=150){if(!r.config.enabled){clearTimeout(o);return}a.isTesting=!0,clearTimeout(o),o=setTimeout(()=>{l()},e)}async function l(){if(a.isTesting=!0,!r.config.enabled||!r.testFilesystem){a.testError=null,a.isTesting=!1;return}app.pb.cancelRequest(n),clearTimeout(s),s=setTimeout(()=>{app.pb.cancelRequest(n),a.testError=Error(`S3 test connection timeout.`),a.isTesting=!1},3e4);try{await app.pb.props.testS3(r.testFilesystem,{requestKey:n}),a.testError=null,a.isTesting=!1}catch(e){e?.isAbort||(a.testError=e,a.isTesting=!1,clearTimeout(s))}}return i.push(watch(()=>r.testFilesystem&&r.config,()=>c())),t.div({pbEvent:`s3Test`,rid:r.rid,hidden:()=>!r.testFilesystem,className:()=>`label s3-test-label txt-nowrap ${a.hasError?`warning`:`success`}`,ariaDescription:app.attrs.tooltip(()=>a.testError?.data?.message),onunmount:()=>{clearTimeout(s),clearTimeout(o),i.forEach(e=>e?.unwatch())}},()=>a.isTesting?t.span({className:`loader sm`}):a.hasError?[t.i({className:`ri-error-warning-line txt-warning`,ariaHidden:!0}),t.span({className:`txt`},`Failed to establish S3 connection`)]:[t.i({className:`ri-checkbox-circle-line txt-success`,ariaHidden:!0}),t.span({className:`txt`},`S3 connected successfully`)])},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.s3ConfigFields=function(e={}){let n=store({rid:void 0,id:void 0,hidden:void 0,in Line truncated
`),n+=`)`,e.where&&(n+=` WHERE ${e.where}`),n},replaceIndexFields(e,n){let r=app.utils.parseIndex(e);return typeof n==`function`?Object.assign(r,n(r)||{}):Object.assign(r,n||{}),app.utils.buildIndex(r)},replaceIndexColumn(e,n,r){if(n===r)return e;let i=app.utils.parseIndex(e),a=!1;for(let e of i.columns)e.name===n&&(e.name=r,a=!0);return a?app.utils.buildIndex(i):e}};window.app=window.app||{},window.app.utils=f,window.app=window.app||{},window.app.utils=window.app.utils||{},window.app.utils.mimeTypes=[{ext:``,mimeType:`application/octet-stream`},{ext:`.xpm`,mimeType:`image/x-xpixmap`},{ext:`.7z`,mimeType:`application/x-7z-compressed`},{ext:`.zip`,mimeType:`application/zip`},{ext:`.xlsx`,mimeType:`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`},{ext:`.docx`,mimeType:`application/vnd.openxmlformats-officedocument.wordprocessingml.document`},{ext:`.pptx`,mimeType:`application/vnd.openxmlformats-officedocument.presentationml.presentation`},{ext:`.epub`,mimeType:`application/epub+zip`},{ext:`.apk`,mimeType:`application/vnd.android.package-archive`},{ext:`.jar`,mimeType:`application/jar`},{ext:`.odt`,mimeType:`application/vnd.oasis.opendocument.text`},{ext:`.ott`,mimeType:`application/vnd.oasis.opendocument.text-template`},{ext:`.ods`,mimeType:`application/vnd.oasis.opendocument.spreadsheet`},{ext:`.ots`,mimeType:`application/vnd.oasis.opendocument.spreadsheet-template`},{ext:`.odp`,mimeType:`application/vnd.oasis.opendocument.presentation`},{ext:`.otp`,mimeType:`application/vnd.oasis.opendocument.presentation-template`},{ext:`.odg`,mimeType:`application/vnd.oasis.opendocument.graphics`},{ext:`.otg`,mimeType:`application/vnd.oasis.opendocument.graphics-template`},{ext:`.odf`,mimeType:`application/vnd.oasis.opendocument.formula`},{ext:`.odc`,mimeType:`application/vnd.oasis.opendocument.chart`},{ext:`.sxc`,mimeType:`application/vnd.sun.xml.calc`},{ext:`.pdf`,mimeType:`application/pdf`},{ext:`.fdf`,mimeType:`application/vnd.fdf`},{ext:``,mimeType:`application/x-ole-storage`},{ext:`.msi`,mimeType:`application/x-ms-installer`},{ext:`.aaf`,mimeType:`application/octet-stream`},{ext:`.msg`,mimeType:`application/vnd.ms-outlook`},{ext:`.xls`,mimeType:`application/vnd.ms-excel`},{ext:`.pub`,mimeType:`application/vnd.ms-publisher`},{ext:`.ppt`,mimeType:`application/vnd.ms-powerpoint`},{ext:`.doc`,mimeType:`application/msword`},{ext:`.ps`,mimeType:`application/postscript`},{ext:`.psd`,mimeType:`image/vnd.adobe.photoshop`},{ext:`.p7s`,mimeType:`application/pkcs7-signature`},{ext:`.ogg`,mimeType:`application/ogg`},{ext:`.oga`,mimeType:`audio/ogg`},{ext:`.ogv`,mimeType:`video/ogg`},{ext:`.png`,mimeType:`image/png`},{ext:`.png`,mimeType:`image/vnd.mozilla.apng`},{ext:`.jpg`,mimeType:`image/jpeg`},{ext:`.jxl`,mimeType:`image/jxl`},{ext:`.jp2`,mimeType:`image/jp2`},{ext:`.jpf`,mimeType:`image/jpx`},{ext:`.jpm`,mimeType:`image/jpm`},{ext:`.jxs`,mimeType:`image/jxs`},{ext:`.gif`,mimeType:`image/gif`},{ext:`.webp`,mimeType:`image/webp`},{ext:`.exe`,mimeType:`application/vnd.microsoft.portable-executable`},{ext:``,mimeType:`application/x-elf`},{ext:``,mimeType:`application/x-object`},{ext:``,mimeType:`application/x-executable`},{ext:`.so`,mimeType:`application/x-sharedlib`},{ext:``,mimeType:`application/x-coredump`},{ext:`.a`,mimeType:`application/x-archive`},{ext:`.deb`,mimeType:`application/vnd.debian.binary-package`},{ext:`.tar`,mimeType:`application/x-tar`},{ext:`.xar`,mimeType:`application/x-xar`},{ext:`.bz2`,mimeType:`application/x-bzip2`},{ext:`.fits`,mimeType:`application/fits`},{ext:`.tiff`,mimeType:`image/tiff`},{ext:`.bmp`,mimeType:`image/bmp`},{ext:`.ico`,mimeType:`image/x-icon`},{ext:`.mp3`,mimeType:`audio/mpeg`},{ext:`.flac`,mimeType:`audio/flac`},{ext:`.midi`,mimeType:`audio/midi`},{ext:`.ape`,mimeType:`audio/ape`},{ext:`.mpc`,mimeType:`audio/musepack`},{ext:`.amr`,mimeType:`audio/amr`},{ext:`.wav`,mimeType:`audio/wav`},{ext:`.aiff`,mimeType:`audio/aiff`},{ext:`.au`,mimeType:`audio/basic`},{ext:`.mpeg`,mimeType:`video/mpeg`},{ext:`.mov`,mimeType:`video/quicktime`},{ext:`.mp4`,mimeType:`video/mp4`},{ext:`.avif`,mimeType:`image/avif`},{ext:`.3gp`,mimeType:`video/3gpp`},{ext:`.3g2`,mimeType:`video/3gpp2`},{ext:`.mp4`,mimeType:`audio/mp4`},{ext:`.mqv`,mimeType:`video/quicktime`},{ext:`.m4a`,mimeType:`audio/x-m4a`},{ext:`.m4v`,mimeType:`video/x-m4v`},{ext:`.heic`,mimeType:`image/heic`},{ext:`.heic`,mimeType:`image/heic-sequence`},{ext:`.heif`,mimeType:`image/heif`},{ext:`.heif`,mimeType:`image/heif-sequence`},{ext:`.mj2`,mimeType:`video/mj2`},{ext:`.dvb`,mimeType:`video/vnd.dvb.file`},{ext:`.webm`,mimeType:`video/webm`},{ext:`.avi`,mimeType:`video/x-msvideo`},{ext:`.flv`,mimeType:`video/x-flv`},{ext:`.mkv`,mimeType:`video/x-matroska`},{ext:`.asf`,mimeType:`video/x-ms-asf`},{ext:`.asf`,mimeType:`video/x-ms-wmv`},{ext:`.asf`,mimeType:`video/asf`},{ext:`.aac`,mimeType:`audio/aac`},{ext:`.voc`,mimeType:`audio/x-unknown`},{ext:`.m3u`,mimeType:`application/vnd.apple.mpegurl`},{ext:`.rmvb`,mimeType:`application/Line truncated
`)}o&&(n.classList.add(`error`),r.__errListener=function(){te(r,n),app.utils.deleteByPath(app.store.errors,i)},r.addEventListener(`input`,r.__errListener),r.addEventListener(`change`,r.__errListener),r.setAttribute(`data-error`,!0),r.setCustomValidity&&r.reportValidity&&r.classList.contains(`inline-error`)?(r.setCustomValidity(o),r.reportValidity(),r._oldTitle=r.title,r.title=o):n.after(t.div({"html-data-input-name":i,className:`field-help error generated-error`,textContent:o})))}}),p.onmessage=e=>{e.data?.collections&&JSON.stringify(app.store.collections)!=JSON.stringify(e.data.collections)&&(app.store.collections=e.data.collections),e.data?.settings&&JSON.stringify(app.store.settings)!=JSON.stringify(e.data.settings)&&(app.store.settings=e.data.settings),e.data?.colorScheme&&(app.store.userColorScheme=e.data.colorScheme)},watch(()=>JSON.stringify(app.store.collections),(e,n)=>{e&&e!=`[]`&&n&&n!=`[]`&&e!=n&&p?.postMessage({collections:JSON.parse(e)})}),watch(()=>JSON.stringify(app.store.settings),(e,n)=>{e&&e!=`{}`&&n&&n!=`{}`&&e!=n&&p?.postMessage({settings:JSON.parse(e)}),window.localStorage.setItem(m,e)});var y=class e extends Error{constructor(n){super(`ClientResponseError`),this.url=``,this.status=0,this.response={},this.isAbort=!1,this.originalError=null,Object.setPrototypeOf(this,e.prototype),typeof n==`object`&&n&&(this.originalError=n.originalError,this.url=typeof n.url==`string`?n.url:``,this.status=typeof n.status==`number`?n.status:0,this.isAbort=!!n.isAbort||n.name===`AbortError`||n.message===`Aborted`,this.response=n.response!==null&&typeof n.response==`object`?n.response:n.data!==null&&typeof n.data==`object`?n.data:{}),this.originalError||n instanceof e||(this.originalError=n),this.name=`ClientResponseError `+this.status,this.message=this.response?.message,this.message||=this.isAbort?`The request was aborted (most likely autocancelled; you can find more info in https://github.com/pocketbase/js-sdk#auto-cancellation).`:this.originalError?.cause?.message?.includes(`ECONNREFUSED ::1`)?`Failed to connect to the PocketBase server. Try changing the SDK URL from localhost to 127.0.0.1 (https://github.com/pocketbase/js-sdk/issues/21).`:`Something went wrong.`,this.cause=this.originalError}get data(){return this.response}toJSON(){return{...this}}},ne=/^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;function re(e,n){let r={};if(typeof e!=`string`)return r;let i=Object.assign({},n||{}).decode||ae,a=0;for(;a<e.length;){let n=e.indexOf(`=`,a);if(n===-1)break;let o=e.indexOf(`;`,a);if(o===-1)o=e.length;else if(o<n){a=e.lastIndexOf(`;`,n-1)+1;continue}let s=e.slice(a,n).trim();if(r[s]===void 0){let a=e.slice(n+1,o).trim();a.charCodeAt(0)===34&&(a=a.slice(1,-1));try{r[s]=i(a)}catch{r[s]=a}}a=o+1}return r}function ie(e,n,r){let i=Object.assign({},r||{}),a=i.encode||b;if(!ne.test(e))throw TypeError(`argument name is invalid`);let o=a(n);if(o&&!ne.test(o))throw TypeError(`argument val is invalid`);let s=e+`=`+o;if(i.maxAge!=null){let e=i.maxAge-0;if(isNaN(e)||!isFinite(e))throw TypeError(`option maxAge is invalid`);s+=`; Max-Age=`+Math.floor(e)}if(i.domain){if(!ne.test(i.domain))throw TypeError(`option domain is invalid`);s+=`; Domain=`+i.domain}if(i.path){if(!ne.test(i.path))throw TypeError(`option path is invalid`);s+=`; Path=`+i.path}if(i.expires){if(!function(e){return Object.prototype.toString.call(e)===`[object Date]`||e instanceof Date}(i.expires)||isNaN(i.expires.valueOf()))throw TypeError(`option expires is invalid`);s+=`; Expires=`+i.expires.toUTCString()}if(i.httpOnly&&(s+=`; HttpOnly`),i.secure&&(s+=`; Secure`),i.priority)switch(typeof i.priority==`string`?i.priority.toLowerCase():i.priority){case`low`:s+=`; Priority=Low`;break;case`medium`:s+=`; Priority=Medium`;break;case`high`:s+=`; Priority=High`;break;default:throw TypeError(`option priority is invalid`)}if(i.sameSite)switch(typeof i.sameSite==`string`?i.sameSite.toLowerCase():i.sameSite){case!0:s+=`; SameSite=Strict`;break;case`lax`:s+=`; SameSite=Lax`;break;case`strict`:s+=`; SameSite=Strict`;break;case`none`:s+=`; SameSite=None`;break;default:throw TypeError(`option sameSite is invalid`)}return s}function ae(e){return e.indexOf(`%`)===-1?e:decodeURIComponent(e)}function b(e){return encodeURIComponent(e)}var x=typeof navigator<`u`&&navigator.product===`ReactNative`||typeof global<`u`&&global.HermesInternal,oe;function S(e){if(e)try{let n=decodeURIComponent(oe(e.split(`.`)[1]).split(``).map((function(e){return`%`+(`00`+e.charCodeAt(0).toString(16)).slice(-2)})).join(``));return JSON.parse(n)||{}}catch{}return{}}function se(e,n=0){let r=S(e);return!(Object.keys(r).length>0&&(!r.exp||r.exp-n>Date.now()/1e3))}oe=typeof atob!=`function`||x?e=>{let n=String(e).replace(/=+$/,``);if(n.length%4==1)throw Error(`'atob' failed: The string to be decoded is not correctly encoded.`);for(var r,i,a=0,o=0,s=``;i=n.charAt(o++);~i&&(r=a%4?64*r+i:i,a++%4)&&(s+=String.fromCharCode(255&r>>(-2*a&6))))i=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=`.in Line truncated
`+ft:null),"html-class":`formatted-date`,className:()=>`formatted-date ${n.short?`short`:`full`}`,onunmount:()=>{r.forEach(e=>e?.unwatch())}},()=>{if(!n.value)return t.span({className:`missing-value`});if(n.short){let e=n.value.split(` `);return[t.span({className:`primary-date`},e[0]),t.span({className:`secondary-date`},e[1])]}return[t.span({className:`primary-date`},app.utils.toLocalDatetime(n.value)),t.span({className:`secondary-date`},n.value)]})},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.refreshButton=function(e={}){let n=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,tooltip:`Refresh`,className:`btn transparent secondary circle rotate-btn`,disabled:!1,onclick:function(e){}}),r=app.utils.extendStore(n,e),i,a=t.button({rid:n.rid,id:()=>n.id,hidden:()=>n.hidden,inert:()=>n.inert,type:`button`,ariaLabel:app.attrs.tooltip(()=>n.tooltip),disabled:()=>n.disabled,className:()=>n.className,onunmount:()=>{clearTimeout(i),r.forEach(e=>e?.unwatch())},onclick:e=>{e.preventDefault(),n.onclick&&n.onclick(e),a.dataset.rotate=!0,a.addEventListener(`animationend`,()=>{a.dataset.rotate=!1}),clearTimeout(i),i=setTimeout(()=>{clearTimeout(i),a.dataset.rotate=!1},500)}},t.i({className:`ri-refresh-line`,ariaHidden:!0}));return a},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.searchHistoryButton=function(e={}){let n=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,value:void 0,historyKey:`default`,max:15,openInNewTabParam:`filter`,btnClassName:`btn sm pill secondary transparent p-r-5`,onselect:function(e){}}),r=app.utils.extendStore(n,e),i=store({items:app.utils.getLocalHistory(n.historyKey,[])});function a(e){o(e),i.items.unshift(e)}function o(e){app.utils.removeByValue(i.items,e)}let s=`history_dropdown_`+app.utils.randomString();r.push(watch(()=>n.value,e=>{e&&a(e)})),r.push(watch(()=>{i.items.length>n.max&&(i.items=i.items.slice(0,n.max)),app.utils.saveLocalHistory(n.historyKey,i.items)}));let c=t.div({id:s,className:`dropdown sm left nowrap history-searchbar-dropdown`,popover:`hint`,onclick:e=>(e.stopPropagation(),!1)},t.div({className:`block p-5`},t.small({className:`txt-hint`},`Search history`)),()=>i.items?.length?i.items.slice(0,n.max).map(e=>t.button({type:`button`,className:`dropdown-item txt-code`,onclick:()=>{c.hidePopover(),n.onselect?.(e),a(e)},onauxclick:()=>{if(n.openInNewTabParam){a(e),c.hidePopover();let r=app.utils.replaceHashQueryParams({[n.openInNewTabParam]:e},!1);window.open(r,`_blank`)}}},t.span({className:`txt-ellipsis`,title:e,textContent:e}),t.small({role:`button`,className:`remove-btn link-hint m-l-auto p-l-5 p-r-5`,title:`Clear`,onauxclick:e=>(e.stopPropagation(),!1),onclick:n=>(n.stopPropagation(),o(e),!1)},t.i({className:`ri-close-line`,ariaHidden:!0})))):t.div({rid:`no-history`,className:`block p-5`},t.span(null,`Your recent searches will show up here.`)));return t.button({rid:n.rid,id:()=>n.id,hidden:()=>n.hidden,inert:()=>n.inert,type:`button`,title:`Search history`,className:()=>n.btnClassName,"html-popovertarget":s,onunmount:()=>{r?.forEach(e=>e?.unwatch())}},t.i({className:`ri-search-line`,ariaHidden:!0}),t.i({className:`ri-arrow-drop-down-line`,ariaHidden:!0}),c)},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.s3Test=function(e={}){let n=`s3_test_request`,r=store({rid:void 0,config:null,label:`Use S3 storage`,testFilesystem:`storage`}),i=app.utils.extendStore(r,e),a=store({isTesting:!1,testError:null,get hasError(){return!app.utils.isEmpty(a.testError)}}),o,s;function c(e=150){if(!r.config.enabled){clearTimeout(o);return}a.isTesting=!0,clearTimeout(o),o=setTimeout(()=>{l()},e)}async function l(){if(a.isTesting=!0,!r.config.enabled||!r.testFilesystem){a.testError=null,a.isTesting=!1;return}app.pb.cancelRequest(n),clearTimeout(s),s=setTimeout(()=>{app.pb.cancelRequest(n),a.testError=Error(`S3 test connection timeout.`),a.isTesting=!1},3e4);try{await app.pb.props.testS3(r.testFilesystem,{requestKey:n}),a.testError=null,a.isTesting=!1}catch(e){e?.isAbort||(a.testError=e,a.isTesting=!1,clearTimeout(s))}}return i.push(watch(()=>r.testFilesystem&&r.config,()=>c())),t.div({pbEvent:`s3Test`,rid:r.rid,hidden:()=>!r.testFilesystem,className:()=>`label s3-test-label txt-nowrap ${a.hasError?`warning`:`success`}`,ariaDescription:app.attrs.tooltip(()=>a.testError?.data?.message),onunmount:()=>{clearTimeout(s),clearTimeout(o),i.forEach(e=>e?.unwatch())}},()=>a.isTesting?t.span({className:`loader sm`}):a.hasError?[t.i({className:`ri-error-warning-line txt-warning`,ariaHidden:!0}),t.span({className:`txt`},`Failed to establish S3 connection`)]:[t.i({className:`ri-checkbox-circle-line txt-success`,ariaHidden:!0}),t.span({className:`txt`},`S3 connected successfully`)])},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.s3ConfigFields=function(e={}){let n=store({rid:void 0,id:void 0,hidden:void 0,in Line truncated
<video controls width="300">
<source src="${a}" />
<p>Download: <a href="${a}" download="${i}">${i}</a>.</p>
@@ -43,7 +43,7 @@ var e=Object.create,n=Object.defineProperty,r=Object.getOwnPropertyDescriptor,i=
// load the token into the store
final token = '...';
pb.authStore.save(token, null);
`,footnote:t.div({className:`txt-right`},t.a({href:`https://github.com/pocketbase/dart-sdk`,target:`_blank`,rel:`noopener noreferrer`,textContent:`Dart SDK docs`}))}]})),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,disabled:()=>r.isLoading,onclick:()=>app.modals.close()},t.span({className:`txt`},`Close`)),t.button({hidden:()=>r.token,type:`submit`,"html-form":n+`_form`,className:()=>`btn expanded-lg ${r.isLoading?`loading`:``}`,disabled:()=>r.isLoading},t.span({className:`txt`},`Generate token`)),t.button({hidden:()=>!r.token,type:`button`,className:()=>`btn secondary expanded-lg ${r.isLoading?`loading`:``}`,onclick:()=>o()},t.span({className:`txt`},`Generate new one`))))}window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openRecordUpsert=function(e,n=null,r={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null,onsave:null,ondelete:null,onduplicate:null,ontokensreset:null,onpasswordresetsend:null,onverificationsend:null}){app.store.errors=null;let i=tn(e,n,r);i&&(document.body.appendChild(i),app.modals.open(i))};var $t=`main`,en=`authProviders`;function tn(e,n,r){if(!e?.id){console.warn(`[recordUpsertModal] missing required collection`);return}let i,a=`record_upsert_`+app.utils.randomString(),o=app.utils.getLocalHistory(app.consts.COLUMNS_STORAGE_PREFIX+e.id,{}),s=store({isLoading:!0,isSaving:!1,isLocked:!1,originalRecord:{},record:{},initialDraft:null,activeTab:$t,get isNew(){return app.utils.isEmpty(s.originalRecord?.id)},get isAuthCollection(){return e.type==`auth`},get isSuperusersCollection(){return e.name==`_superusers`},get showTabs(){return!s.isNew&&s.isAuthCollection&&!s.isSuperusersCollection},get excludedFields(){let e=[`id`];return s.isAuthCollection&&e.push(`email`,`emailVisibility`,`verified`,`password`,`tokenKey`),e},get initialDraftHash(){return l(s.initialDraft)},get recordHash(){return l(s.record)},get originalRecordHash(){return l(s.originalRecord)},get hasChanges(){return s.originalRecordHash!=s.recordHash||s.isAuthCollection&&(!!s.record.password||!!s.record.passwordConfirm)},get isFormDisabled(){return s.isLoading||s.isSaving||!s.isNew&&!s.hasChanges}});function c(e,n){switch(e){case`expand`:return;case`password`:case`passwordConfirm`:case`tokenKey`:return s.isAuthCollection?void 0:n}return n}function l(e){return e?JSON.stringify(e,c):``}function u(){return`draft_`+e.id+`_`+(s.originalRecord?.id||``)}function d(){return window.localStorage.getItem(u())||``}function f(){try{let e=d();if(e)return JSON.parse(e)}catch(e){console.warn(`getDraft failure:`,e),m()}return null}function p(e){try{window.localStorage.setItem(u(),e)}catch(e){console.warn(`saveDraft failure:`,e),m()}}function m(){window.localStorage.removeItem(u()),s.initialDraft=null}function h(){if(!s.initialDraft)return;app.store.errors&&(app.store.errors=null);let e=JSON.parse(JSON.stringify(s.initialDraft));m(),s.record=e}let g;function _(){s.initialDraft=f(),g?.unwatch(),g=watch(()=>s.recordHash,(e,n)=>{n!==void 0&&(s.hasChanges?p(e):m())})}async function v(n){if(s.isLoading=!0,g?.unwatch(),n=app.utils.isObject(n)?n:{id:n||``},!n.id){s.originalRecord=JSON.parse(JSON.stringify(n)),s.record=JSON.parse(JSON.stringify(n)),s.isLoading=!1,s.isLocked=!1,_();return}try{s.isLocked=!!app.store.settings?.meta?.hideControls,s.originalRecord=JSON.parse(JSON.stringify(n)),s.record=JSON.parse(JSON.stringify(n));let r=await app.pb.collection(e.name).getOne(n.id,{requestKey:`upsert_load_`+n.id});n.expand&&(r.expand=JSON.parse(JSON.stringify(n.expand))),Object.assign(s.originalRecord,JSON.parse(JSON.stringify(r))),Object.assign(s.record,JSON.parse(JSON.stringify(r))),s.isLoading=!1,setTimeout(()=>{_()},0)}catch(e){e?.isAbort||(app.checkApiError(e),s.isLoading=!1,setTimeout(()=>app.modals.close(i,!0),0))}}function ee(e){for(let n in e)(n.startsWith(`@@`)||s.isAuthCollection&&(n==`password`||n==`passwordConfirm`))&&delete e[n]}async function te(){let n={};for(let e in s.record){if(e==`expand`||e.startsWith(`@@`))continue;let r=s.record[e]?.__raw||s.record[e];r===void 0&&(r=null),n[e]=r}for(let r of e.fields){let i=app.fieldTypes[r.type]?.onrecordsave;i&&await i({collection:e,originalRecord:s.originalRecord,record:s.record,field:r,payload:n})}return n}async function y(n=!0){if(!(s.isLocked||s.isSaving||!s.isNew&&!s.hasChanges)){s.isSaving=!0;try{let a=await te(),o=app.utils.isEmpty(s.originalRecord?.id),c;c=o?await app.pb.collection(e.name).create(a):await app.pb.collection(e.name).update(s.originalRecord.id,a),m(),o?(s.originalRecord=structuredClone(c),s.record=structuredClone(c)):(Object.assign(s.originalRecord,structuredClone(c)),Object.assign(s.record,structuredClone(c)),ee(s.originalRecord),ee(s.record)),r.onsave?.(c,o),app.store.errors=null;let l;l=o?`Successfully created ${e.name} "${c.id}".`:`Successfully updated ${e.name} "${c.id}".`,app.toasts.success(l,{key:`recordSave`}),s.isSaving=!1,n&&app.modals.close(i,!0 Line truncated
`,footnote:t.div({className:`txt-right`},t.a({href:`https://github.com/pocketbase/dart-sdk`,target:`_blank`,rel:`noopener noreferrer`,textContent:`Dart SDK docs`}))}]})),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,disabled:()=>r.isLoading,onclick:()=>app.modals.close()},t.span({className:`txt`},`Close`)),t.button({hidden:()=>r.token,type:`submit`,"html-form":n+`_form`,className:()=>`btn expanded-lg ${r.isLoading?`loading`:``}`,disabled:()=>r.isLoading},t.span({className:`txt`},`Generate token`)),t.button({hidden:()=>!r.token,type:`button`,className:()=>`btn secondary expanded-lg ${r.isLoading?`loading`:``}`,onclick:()=>o()},t.span({className:`txt`},`Generate new one`))))}window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openRecordUpsert=function(e,n=null,r={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null,onsave:null,ondelete:null,onduplicate:null,ontokensreset:null,onpasswordresetsend:null,onverificationsend:null}){app.store.errors=null;let i=tn(e,n,r);i&&(document.body.appendChild(i),app.modals.open(i))};var $t=`main`,en=`authProviders`;function tn(e,n,r){if(!e?.id){console.warn(`[recordUpsertModal] missing required collection`);return}let i,a=`record_upsert_`+app.utils.randomString(),o=app.utils.getLocalHistory(app.consts.COLUMNS_STORAGE_PREFIX+e.id,{}),s=store({isLoading:!0,isSaving:!1,isLocked:!1,originalRecord:{},record:{},initialDraft:null,activeTab:$t,get isNew(){return app.utils.isEmpty(s.originalRecord?.id)},get isAuthCollection(){return e.type==`auth`},get isSuperusersCollection(){return e.name==`_superusers`},get showTabs(){return!s.isNew&&s.isAuthCollection&&!s.isSuperusersCollection},get excludedFields(){let e=[`id`];return s.isAuthCollection&&e.push(`email`,`emailVisibility`,`verified`,`password`,`tokenKey`),e},get initialDraftHash(){return l(s.initialDraft)},get recordHash(){return l(s.record)},get originalRecordHash(){return l(s.originalRecord)},get hasChanges(){return s.originalRecordHash!=s.recordHash||s.isAuthCollection&&(!!s.record.password||!!s.record.passwordConfirm)},get isFormDisabled(){return s.isLoading||s.isSaving||!s.isNew&&!s.hasChanges}});function c(e,n){switch(e){case`expand`:return;case`password`:case`passwordConfirm`:case`tokenKey`:return s.isAuthCollection?void 0:n}return n}function l(e){return e?JSON.stringify(e,c):``}function u(){return`draft_`+e.id+`_`+(s.originalRecord?.id||``)}function d(){return window.localStorage.getItem(u())||``}function f(){try{let e=d();if(e)return JSON.parse(e)}catch(e){console.warn(`getDraft failure:`,e),m()}return null}function p(e){try{window.localStorage.setItem(u(),e)}catch(e){console.warn(`saveDraft failure:`,e),m()}}function m(){window.localStorage.removeItem(u()),s.initialDraft=null}function h(){if(!s.initialDraft)return;app.store.errors&&(app.store.errors=null);let e=JSON.parse(JSON.stringify(s.initialDraft));m(),s.record=e}let g;function _(){s.initialDraft=f(),g?.unwatch(),g=watch(()=>s.recordHash,(e,n)=>{n!==void 0&&(s.hasChanges?p(e):m())})}async function v(n){if(s.isLoading=!0,g?.unwatch(),n=app.utils.isObject(n)?n:{id:n||``},!n.id){s.originalRecord=JSON.parse(JSON.stringify(n)),s.record=JSON.parse(JSON.stringify(n)),s.isLoading=!1,s.isLocked=!1,_();return}try{s.isLocked=!!app.store.settings?.meta?.hideControls,s.originalRecord=JSON.parse(JSON.stringify(n)),s.record=JSON.parse(JSON.stringify(n));let r=await app.pb.collection(e.name).getOne(n.id,{requestKey:`upsert_load_`+n.id});n.expand&&(r.expand=JSON.parse(JSON.stringify(n.expand))),Object.assign(s.originalRecord,JSON.parse(JSON.stringify(r))),Object.assign(s.record,JSON.parse(JSON.stringify(r))),s.isLoading=!1,setTimeout(()=>{_()},0)}catch(e){e?.isAbort||(app.checkApiError(e),s.isLoading=!1,setTimeout(()=>app.modals.close(i,!0),0))}}function ee(e){for(let n in e)(n.startsWith(`@@`)||s.isAuthCollection&&(n==`password`||n==`passwordConfirm`))&&delete e[n]}async function te(){let n={};for(let e in s.record){if(e==`expand`||e.startsWith(`@@`))continue;let r=s.record[e]?.__raw||s.record[e];r===void 0&&(r=null),n[e]=r}for(let r of e.fields){let i=app.fieldTypes[r.type]?.onrecordsave;i&&await i({collection:e,originalRecord:s.originalRecord,record:s.record,field:r,payload:n})}return n}async function y(n=!0){if(!(s.isLocked||s.isSaving||!s.isNew&&!s.hasChanges)){s.isSaving=!0;try{let a=await te(),o=app.utils.isEmpty(s.originalRecord?.id),c;c=o?await app.pb.collection(e.name).create(a):await app.pb.collection(e.name).update(s.originalRecord.id,a),m(),o?(s.originalRecord=structuredClone(c),s.record=structuredClone(c)):(Object.assign(s.originalRecord,structuredClone(c)),Object.assign(s.record,structuredClone(c)),ee(s.originalRecord),ee(s.record)),r.onsave?.(c,o),app.store.errors=null;let l;l=o?`Successfully created ${e.name} "${c.id}".`:`Successfully updated ${e.name} "${c.id}".`,app.toasts.success(l,{key:`recordSave`}),s.isSaving=!1,n&&app.modals.close(i,!0 Line truncated
The field cannot be presentable if hidden.`),n})})))},()=>{if(i.showHidden)return t.div({className:`field prop-hidden`},t.input({type:`checkbox`,id:r+`.hidden`,className:`sm`,name:()=>`fields.${e.fieldIndex}.hidden`,checked:()=>!!e.field.hidden,onchange:n=>e.field.hidden=n.target.checked}),t.label({htmlFor:r+`.hidden`},t.span({className:`txt`},`Hidden`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Hide from the JSON API response and filters.`)})))},t.button({hidden:()=>!i.showDuplicate&&(!i.showRemove||e.field.system),type:`button`,title:`More options`,className:`btn sm circle transparent secondary more-btn m-l-auto`,"html-popovertarget":r+`_options_dropdown`},t.i({className:`ri-more-line`,ariaHidden:!0})),t.div({id:r+`_options_dropdown`,className:`dropdown sm field-options-dropdown`,popover:`auto`},()=>{if(i.showDuplicate)return t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,textContent:`Duplicate`,onclick:e=>{o(),e.target.closest(`.dropdown`).hidePopover()}})},()=>{if(!(!i.showRemove||e.field.system))return t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,textContent:`Remove`,onclick:e=>{s(),e.target.closest(`.dropdown`).hidePopover(),e.target.closest(`details`).open=!1}})})))};function Cn(e,n=`field`){let r=n,i=2,a=n.match(/\d+$/)?.[0]||``,o=a?n.substring(0,n.length-a.length):n;for(;e?.find(e=>e.name.toLowerCase()==r.toLowerCase());)r=o+((a<<0)+i),i++;return r}function wn(e,n,r){if(!r||n===void 0||n===r||!e?.passwordAuth?.identityFields?.length)return;let i=e.passwordAuth.identityFields;for(let e=0;e<i.length;e++)i[e]==n&&(i[e]=r)}function Tn(e,n,r){!r||n===void 0||n===r||!e?.indexes?.length||!e?.fields?.length||e.fields.find(e=>!e[`@toDelete`]&&e.name==n)||(e.indexes=e.indexes.map(e=>app.utils.replaceIndexColumn(e,n,r)))}function En(e){let n=`text_`+app.utils.randomString(),r=store({get hasAutogenerate(){return!app.utils.isEmpty(e.field.autogeneratePattern)&&app.utils.isEmpty(e.originalRecord?.id)},get isDisabled(){return!app.utils.isEmpty(e.originalRecord?.id)&&e.field.primaryKey},get isRequired(){return e.field.required&&!r.hasAutogenerate&&!r.isDisabled}});return t.div({className:`record-field-input field-type-text`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:n},t.i({ariaHidden:!0,className:()=>e.field.primaryKey?`ri-key-line`:app.fieldTypes.text.icon}),t.span({className:`txt`},()=>e.field.name)),t.div({className:`autoexpand-wrapper`},t.textarea({id:n,rows:1,name:()=>e.field.name,required:()=>r.isRequired,disabled:()=>r.isDisabled,placeholder:()=>r.hasAutogenerate?`Leave empty to autogenerate...`:``,value:()=>e.record[e.field.name]||``,oninput:n=>e.record[e.field.name]=n.target.value||``}),t.div({className:`input`},()=>e.record[e.field.name],` `))),()=>{if(!e.field.primaryKey||!e.originalRecord?.id)return;let n=e.collection?.fields?.filter(e=>e.type==`autodate`)||[];if(!n.length)return;let r=[];for(let i of n)r.push(`${i.name}: ${app.utils.displayValue(e.record[i.name])}`);return t.div({className:`field addon`},t.i({className:`ri-information-line txt-hint link-faded`,ariaDescription:app.attrs.tooltip(r.join(`
`),`left`)}))}),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function Dn(e){let n=`f_`+app.utils.randomString();return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.min`},t.span({className:`txt`},`Min length`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Clear the field or set it to 0 for no limit.`)})),t.input({type:`number`,id:n+`.min`,name:()=>`fields.${e.fieldIndex}.min`,step:1,min:0,max:2**53-1,placeholder:`No min limit`,value:()=>e.field.min||``,oninput:n=>{n.target.value?.length>1&&n.target.value[0]==`0`||(e.field.min=parseInt(n.target.value,10))},onchange:n=>{e.field.min=null,e.field.min=parseInt(n.target.value,10)}}))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.max`},t.span({className:`txt`},`Max length`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Clear the field or set it to 0 to fallback to the default limit.`)})),t.input({type:`number`,id:n+`.max`,name:()=>`fields.${e.fieldIndex}.max`,step:1,min:()=>e.field.min||0,max:2**53-1,placeholder:`Default to max 5000 characters`,value:()=>e.field.max||``,oninput:n=>{n.target.value?.length>1&&n.target.value[0]==`0`||(e.field.max=parseInt(n.target.value,10))},onchange:n=>{e.field.max=null,e.field.max=parseInt(n.target.value,10)}}))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.pattern`},t.span({className:`txt`},`Validation pattern`),()=>{if(e.field.primaryKey)return t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`All record ids have forbidden characters and unique case-insensitive (ASCII) validations in addition to the user defined regex pattern.`)})}),t.input({type:`text`,id:n+`.pattern`,name:()=>`fields.${e.fieldIndex}.pattern`,value:()=>e.field.pattern||``,oninput:n=>e.field.pattern=n.target.value})),t.div({className:`field-help`},`Ex. `,t.code(null,`^[a-z0-9]+$`))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.autogeneratePattern`},t.span({className:`txt`},`Autogenerate pattern`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Set and autogenerate text matching the pattern on missing record create value.`)})),t.input({type:`text`,id:n+`.autogeneratePattern`,name:()=>`fields.${e.fieldIndex}.autogeneratePattern`,value:()=>e.field.autogeneratePattern||``,oninput:n=>e.field.autogeneratePattern=n.target.value})),t.div({className:`field-help`},`Ex. `,t.code(null,`[a-z0-9]{30}`))),t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`},`(!='')`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Requires the field value to be nonempty string.`)})))]})}function On(e){return t.div({className:`record-field-view field-type-text`},()=>{let n=e.record[e.field.name]||``;if(n==``)return t.span({className:`missing-value`});if(e.field.primaryKey){let r=null;return e.record?.collectionName==`_superusers`&&app.store.superuser?.id==e.record?.id&&(r=t.strong({className:`txt`,textContent:` (you)`})),t.span({className:`label`},app.components.copyButton(n),t.span({className:`txt-ellipsis`},app.utils.truncate(n),r))}return e.short?t.span({className:`txt txt-ellipsis`,textContent:app.utils.truncate(n)}):n})}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.text={icon:`ri-text`,label:`Plain text`,settings:Dn,input:En,view:On,filterModifiers:e=>[`lower`],dummyData:(e,n=!1)=>e.primaryKey?app.utils.randomString(15):`example text`};function kn(e){let n=`editor_`+app.utils.randomString(),r=store({lazyEditor:``}),i;return t.div({className:`record-field-input field-type-editor large-modal`,onmount:()=>{i=setTimeout(()=>{r.lazyEditor=app.components.tinymce({id:n,name:()=>e.field.name,required:()=>e.field.required,convertURLs:()=>e.field.convertURLs,value:()=>e.record[e.field.name]||``,onchange:n=>{e.record[e.field.name]=n}})},0)},onunmount:()=>{clearTimeout(i)}},t.div({className:`field`},t.label({htmlFor:n},t.i({className:app.fieldTypes.editor.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name)),()=>r.lazyEditor),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function An(e){let n=`f_`+app.utils.randomString();return store({showInfo:!1}),app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({c Line truncated
This field is disabled if "Only domains" is set.`)})),t.input({type:`text`,id:n+`.exceptDomains`,disabled:()=>!app.utils.isEmpty(e.field.onlyDomains),name:()=>`fields.${e.fieldIndex}.exceptDomains`,value:()=>app.utils.joinNonEmpty(e.field.exceptDomains),onchange:n=>e.field.exceptDomains=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},`Use comma as separator.`)),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.onlyDomains`},t.span({className:`txt`},`Only domains`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`List of domains that are ONLY allowed.
@@ -83,4 +83,4 @@ Records update will also require an extra unlock step before save.`)})))),t.div(
@weekly
@daily
@midnight
@hourly`)},`macros`),`.`,t.br(),`By default the timezone is in UTC.`)),t.div({className:`col-lg-6`},t.div({className:`field`},t.label({htmlFor:`backups.cronMaxKeep`},`Max @auto backups to keep`),t.input({id:`backups.cronMaxKeep`,name:`backups.cronMaxKeep`,type:`number`,required:()=>a.enableAutoBackups,min:1,value:()=>a.formSettings.backups.cronMaxKeep,oninput:e=>{a.formSettings.backups.cronMaxKeep=parseInt(e.target.value,10)}})))))),t.div({className:`col-lg-12`},app.components.s3ConfigFields({toggleLabel:`Store backups in S3 storage`,testFilesystem:`backups`,config:()=>a.formSettings.backups.s3})),t.div({className:`col-lg-12`},t.hr()),t.div({className:`col-lg-12`},t.div({className:`flex`},t.div({className:`m-r-auto`}),t.button({hidden:()=>!a.hasChanges,type:`button`,className:`btn transparent secondary`,onclick:l},t.span({className:`txt`},`Cancel`)),t.button({className:()=>`btn expanded-lg ${a.isSaving?`loading`:``}`,disabled:()=>!a.hasChanges||a.isSaving},t.span({className:`txt`},`Save changes`))))])))}function zi(e={oncreated:null}){let n=Bi(e);n&&(document.body.appendChild(n),app.modals.open(n))}function Bi(e){let n,r=`backup_create_`+app.utils.randomString(),i=store({name:``,isSubmitting:!1}),a;async function o(){if(!i.isSubmitting){i.isSubmitting=!0,clearTimeout(a),a=setTimeout(()=>{app.modals.close(n)},1500);try{await app.pb.backups.create(i.name,{requestKey:r}),i.isSubmitting=!1,e.oncreated&&e.oncreated(i.name),app.toasts.success(`Successfully generated new backup.`),app.modals.close(n)}catch(e){e.isAbort||(clearTimeout(a),i.isSubmitting=!1,app.checkApiError(e))}}}return n=t.div({pbEvent:`backupCreateModal`,className:`modal popup backup-create-modal`,onbeforeclose:()=>{i.isSubmitting&&app.toasts.info(`The backup was started but may take a while to complete. You can come back later.`)},onafterclose:e=>{clearTimeout(a),e?.remove()}},t.header({className:`modal-header`},t.h5({className:`m-auto txt-center`},`Initialize new backup`)),t.form({id:r,className:`modal-content backup-restore-form`,autocomplete:`off`,onsubmit:e=>{e.preventDefault(),o()}},t.div({className:`grid`},t.div({className:`col-lg-12`},t.div({className:`alert warning`},t.div({className:`content`},t.p(null,`Please note that during the backup the performance could be slightly degraded and some queries may take longer to complete than usual.`),t.p({className:`txt-bold`},`If you are using S3 storage for the collections file upload, you'll have to backup them separately since they are not locally stored and they will not be included in the generated backup!`)))),t.div({className:`col-lg-12`},t.div({className:`field`},t.label({htmlFor:r+`_name`},`Backup name`),t.input({id:r+`_name`,name:`name`,type:`text`,pattern:`^[a-z0-9_-]+.zip$`,placeholder:`Leave empty to autogenerate`,value:()=>i.name,oninput:e=>i.name=e.target.value})),t.div({className:`field-help`},`Must be in the format [a-z0-9_-].zip`)))),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,disabled:()=>i.isSubmitting,onclick:()=>app.modals.close(n)},t.span({className:`txt`},`Cancel`)),t.button({"html-form":r,type:`submit`,className:()=>`btn ${i.isSubmitting?`loading`:``}`,disabled:()=>i.isSubmitting},t.span({className:`txt`},`Start backup`)))),n}function Vi(e){let n=Hi(e);document.body.appendChild(n),app.modals.open(n)}function Hi(e){let n=`backup_restore_`+app.utils.randomString(),r=store({key:e,keyConfirm:``,isSubmitting:!1,get canSubmit(){return r.key&&r.key==r.keyConfirm}}),i;async function a(){if(!(r.isSubmitting||!r.canSubmit)){clearTimeout(i),r.isSubmitting=!0;try{await app.pb.backups.restore(r.keyConfirm),i=setTimeout(()=>{window.location.reload(),r.isSubmitting=!1},2e3)}catch(e){clearTimeout(i),e?.isAbort||(r.isSubmitting=!1,app.checkApiError(e))}}}return t.div({pbEvent:`backupRestoreModal`,className:`modal popup backup-restore-modal`,onbeforeclose:()=>!r.isSubmitting,onafterclose:e=>{e?.remove()},onunmount:()=>{clearTimeout(i)}},t.header({className:`modal-header`},t.h5({className:`m-auto txt-center`},`Restore `,t.strong(null,()=>r.key))),t.form({id:n,className:`modal-content backup-restore-form`,autocomplete:`off`,onsubmit:e=>{e.preventDefault(),a()}},t.div({className:`grid`},t.div({className:`col-lg-12`},t.div({className:`alert danger`},t.div({className:`content`},t.p({className:`txt-bold`},`Please proceed with extreme caution and use it only with trusted backups!`),t.p(null,`Backup restore currently works only on UNIX based systems.`),t.p(null,`The restore operation will attempt to replace your existing `,t.code(null,`pb_data`),` with the one from the backup and will restart the application process.`),t.p(null,`This means that on success all of your data (including app settings, users, superusers, etc.) will be replaced with the ones from the backup.`),t.p(null,`The operation will be reverted if the backup is invalid (ex. missing `,t.code(null,`data.db`),` file).`),t.p(null,`Below is an oversimplified version of the restore flow:Line truncated
@hourly`)},`macros`),`.`,t.br(),`By default the timezone is in UTC.`)),t.div({className:`col-lg-6`},t.div({className:`field`},t.label({htmlFor:`backups.cronMaxKeep`},`Max @auto backups to keep`),t.input({id:`backups.cronMaxKeep`,name:`backups.cronMaxKeep`,type:`number`,required:()=>a.enableAutoBackups,min:1,value:()=>a.formSettings.backups.cronMaxKeep,oninput:e=>{a.formSettings.backups.cronMaxKeep=parseInt(e.target.value,10)}})))))),t.div({className:`col-lg-12`},app.components.s3ConfigFields({toggleLabel:`Store backups in S3 storage`,testFilesystem:`backups`,config:()=>a.formSettings.backups.s3})),t.div({className:`col-lg-12`},t.hr()),t.div({className:`col-lg-12`},t.div({className:`flex`},t.div({className:`m-r-auto`}),t.button({hidden:()=>!a.hasChanges,type:`button`,className:`btn transparent secondary`,onclick:l},t.span({className:`txt`},`Cancel`)),t.button({className:()=>`btn expanded-lg ${a.isSaving?`loading`:``}`,disabled:()=>!a.hasChanges||a.isSaving},t.span({className:`txt`},`Save changes`))))])))}function zi(e={oncreated:null}){let n=Bi(e);n&&(document.body.appendChild(n),app.modals.open(n))}function Bi(e){let n,r=`backup_create_`+app.utils.randomString(),i=store({name:``,isSubmitting:!1}),a;async function o(){if(!i.isSubmitting){i.isSubmitting=!0,clearTimeout(a),a=setTimeout(()=>{app.modals.close(n)},1500);try{await app.pb.backups.create(i.name,{requestKey:r}),i.isSubmitting=!1,e.oncreated&&e.oncreated(i.name),app.toasts.success(`Successfully generated new backup.`),app.modals.close(n)}catch(e){e.isAbort||(clearTimeout(a),i.isSubmitting=!1,app.checkApiError(e))}}}return n=t.div({pbEvent:`backupCreateModal`,className:`modal popup backup-create-modal`,onbeforeclose:()=>{i.isSubmitting&&app.toasts.info(`The backup was started but may take a while to complete. You can come back later.`)},onafterclose:e=>{clearTimeout(a),e?.remove()}},t.header({className:`modal-header`},t.h5({className:`m-auto txt-center`},`Initialize new backup`)),t.form({id:r,className:`modal-content backup-restore-form`,autocomplete:`off`,onsubmit:e=>{e.preventDefault(),o()}},t.div({className:`grid`},t.div({className:`col-lg-12`},t.div({className:`alert warning`},t.div({className:`content`},t.p(null,`Please note that during the backup the performance could be slightly degraded and some queries may take longer to complete than usual.`),t.p({className:`txt-bold`},`If you are using S3 storage for the collections file upload, you'll have to backup them separately since they are not locally stored and they will not be included in the generated backup!`)))),t.div({className:`col-lg-12`},t.div({className:`field`},t.label({htmlFor:r+`_name`},`Backup name`),t.input({id:r+`_name`,name:`name`,type:`text`,pattern:`^[a-z0-9_-]+.zip$`,placeholder:`Leave empty to autogenerate`,value:()=>i.name,oninput:e=>i.name=e.target.value})),t.div({className:`field-help`},`Must be in the format [a-z0-9_-].zip`)))),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,disabled:()=>i.isSubmitting,onclick:()=>app.modals.close(n)},t.span({className:`txt`},`Cancel`)),t.button({"html-form":r,type:`submit`,className:()=>`btn ${i.isSubmitting?`loading`:``}`,disabled:()=>i.isSubmitting},t.span({className:`txt`},`Start backup`)))),n}function Vi(e){let n=Hi(e);document.body.appendChild(n),app.modals.open(n)}function Hi(e){let n=`backup_restore_`+app.utils.randomString(),r=store({key:e,keyConfirm:``,isSubmitting:!1,get canSubmit(){return r.key&&r.key==r.keyConfirm}}),i;async function a(){if(!(r.isSubmitting||!r.canSubmit)){clearTimeout(i),r.isSubmitting=!0;try{await app.pb.backups.restore(r.keyConfirm),i=setTimeout(()=>{window.location.reload(),r.isSubmitting=!1},2e3)}catch(e){clearTimeout(i),e?.isAbort||(r.isSubmitting=!1,app.checkApiError(e))}}}return t.div({pbEvent:`backupRestoreModal`,className:`modal popup backup-restore-modal`,onbeforeclose:()=>!r.isSubmitting,onafterclose:e=>{e?.remove()},onunmount:()=>{clearTimeout(i)}},t.header({className:`modal-header`},t.h5({className:`m-auto txt-center`},`Restore `,t.strong(null,()=>r.key))),t.form({id:n,className:`modal-content backup-restore-form`,autocomplete:`off`,onsubmit:e=>{e.preventDefault(),a()}},t.div({className:`grid`},t.div({className:`col-lg-12`},t.div({className:`alert danger`},t.div({className:`content`},t.p({className:`txt-bold`},`Please proceed with extreme caution and use it only with trusted backups!`),t.p(null,`Backup restore currently works only on UNIX based systems.`),t.p(null,`The restore operation will attempt to replace your existing `,t.code(null,`pb_data`),` with the one from the backup and will restart the application process.`),t.p(null,`This means that on success all of your data (including app settings, users, superusers, etc.) will be replaced with the ones from the backup.`),t.p(null,`The operation will be reverted if the backup is invalid (ex. missing `,t.code(null,`data.db`),` file).`),t.p(null,`Below is an oversimplified version of the restore flow:Line truncated
+1
View File
@@ -0,0 +1 @@
@font-face{font-display:block;font-family:remixicon;src:url(../fonts/remixicon/remixicon.woff2?t=1734404658139)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:normal;font-weight:400;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-regular.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:italic;font-weight:400;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-italic.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:normal;font-weight:600;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-600.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:italic;font-weight:600;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-600italic.woff2)format("woff2")}@font-face{font-display:swap;font-family:IBM Plex Mono;font-style:normal;font-weight:400;src:url(../fonts/ibm-plex-mono/ibm-plex-mono-v20-latin-regular.woff2)format("woff2")}@font-face{font-display:swap;font-family:IBM Plex Mono;font-style:normal;font-weight:600;src:url(../fonts/ibm-plex-mono/ibm-plex-mono-v20-latin-600.woff2)format("woff2")}.ri-24-hours-fill:before{content:""}.ri-24-hours-line:before{content:""}.ri-4k-fill:before{content:""}.ri-4k-line:before{content:""}.ri-a-b:before{content:""}.ri-account-box-fill:before{content:""}.ri-account-box-line:before{content:""}.ri-account-circle-fill:before{content:""}.ri-account-circle-line:before{content:""}.ri-account-pin-box-fill:before{content:""}.ri-account-pin-box-line:before{content:""}.ri-account-pin-circle-fill:before{content:""}.ri-account-pin-circle-line:before{content:""}.ri-add-box-fill:before{content:""}.ri-add-box-line:before{content:""}.ri-add-circle-fill:before{content:""}.ri-add-circle-line:before{content:""}.ri-add-fill:before{content:""}.ri-add-line:before{content:""}.ri-admin-fill:before{content:""}.ri-admin-line:before{content:""}.ri-advertisement-fill:before{content:""}.ri-advertisement-line:before{content:""}.ri-airplay-fill:before{content:""}.ri-airplay-line:before{content:""}.ri-alarm-fill:before{content:""}.ri-alarm-line:before{content:""}.ri-alarm-warning-fill:before{content:""}.ri-alarm-warning-line:before{content:""}.ri-album-fill:before{content:""}.ri-album-line:before{content:""}.ri-alert-fill:before{content:""}.ri-alert-line:before{content:""}.ri-aliens-fill:before{content:""}.ri-aliens-line:before{content:""}.ri-align-bottom:before{content:""}.ri-align-center:before{content:""}.ri-align-justify:before{content:""}.ri-align-left:before{content:""}.ri-align-right:before{content:""}.ri-align-top:before{content:""}.ri-align-vertically:before{content:""}.ri-alipay-fill:before{content:""}.ri-alipay-line:before{content:""}.ri-amazon-fill:before{content:""}.ri-amazon-line:before{content:""}.ri-anchor-fill:before{content:""}.ri-anchor-line:before{content:""}.ri-ancient-gate-fill:before{content:""}.ri-ancient-gate-line:before{content:""}.ri-ancient-pavilion-fill:before{content:""}.ri-ancient-pavilion-line:before{content:""}.ri-android-fill:before{content:""}.ri-android-line:before{content:""}.ri-angularjs-fill:before{content:""}.ri-angularjs-line:before{content:""}.ri-anticlockwise-2-fill:before{content:""}.ri-anticlockwise-2-line:before{content:""}.ri-anticlockwise-fill:before{content:""}.ri-anticlockwise-line:before{content:""}.ri-app-store-fill:before{content:""}.ri-app-store-line:before{content:""}.ri-apple-fill:before{content:""}.ri-apple-line:before{content:""}.ri-apps-2-fill:before{content:""}.ri-apps-2-line:before{content:""}.ri-apps-fill:before{content:""}.ri-apps-line:before{content:""}.ri-archive-drawer-fill:before{content:""}.ri-archive-drawer-line:before{content:""}.ri-archive-fill:before{content:""}.ri-archive-line:before{content:""}.ri-arrow-down-circle-fill:before{content:""}.ri-arrow-down-circle-line:before{content:""}.ri-arrow-down-fill:before{content:""}.ri-arrow-down-line:before{content:""}.ri-arrow-down-s-fill:before{content:""}.ri-arrow-down-s-line:before{content:""}.ri-arrow-drop-down-fill:before{content:""}.ri-arrow-drop-down-line:before{content:""}.ri-arrow-drop-left-fill:before{content:""}.ri-arrow-drop-left-line:before{content:""}.ri-arrow-drop-right-fill:before{content:""}.ri-arrow-drop-right-line:before{content:""}.ri-arrow-drop-up-fill:before{content:""}.ri-arrow-drop-up-line:before{content:""}.ri-arrow-go-back-fill:before{content:""}.ri-arrow-go-back-line:before{content:""}.ri-arrow-go-forward-fill:before{content:""}.ri-arrow-go-forward-line:before{content:""}.ri-arrow-left-circle-fill:before{content:""}.ri-arrow-left-circle-line:before{content:""}.ri-aLine truncated
@@ -1 +1 @@
import{n as e,t as n}from"./index-DiLDboS9.js";function r(r){let i=r.params?.token||``,a=e(i);if(!a.newEmail||!a.collectionId){app.toasts.error(`Invalid or expired email change token.`),window.location.hash=`#/`;return}app.store.title=`Confirm email change`;let o=store({password:``,isSubmitting:!1,isSuccess:!1,showPassword:!1});async function s(){if(o.isSubmitting)return;o.isSubmitting=!0;let e=new n(app.pb.baseURL);try{await e.collection(a.collectionId).confirmEmailChange(i,o.password),o.isSuccess=!0}catch(e){app.checkApiError(e),o.isSuccess=!1}o.isSubmitting=!1}return t.div({pbEvent:`pageConfirmEmailChange`,className:`wrapper sm m-auto p-b-base`},t.header({className:`txt-center m-b-base`},t.img({className:`main-logo`,src:()=>app.store.mainLogo,ariaHidden:!0,alt:`App logo`}),t.h5({className:`m-t-10`},()=>app.store.title)),()=>o.isSuccess?t.div({pbEvent:`confirmEmailChangeAlert`,className:`alert success txt-center`},t.p(null,`The email was successfully changed.`),t.p(null,`You can go back and sign in with your new email address.`)):t.form({pbEvent:`confirmEmailChangeForm`,className:`grid confirm-email-change-form`,onsubmit:e=>{e.preventDefault(),s()}},t.div({className:`col-12`},t.div({className:`content txt-center m-b-sm`},`Type your password to confirm changing your email address to `,t.strong(null,a.newEmail),`:`),t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:`password_confirm`},`Password`),t.input({id:`password_confirm`,name:`password`,required:!0,autofocus:!0,type:()=>o.showPassword?`text`:`password`,value:()=>o.password,oninput:e=>o.password=e.target.value})),t.div({className:`field addon`},t.button({type:`button`,tabIndex:-1,className:`btn sm transparent secondary circle tooltip-right`,ariaLabel:app.attrs.tooltip(()=>o.showPassword?`Hide password`:`Show password`),onclick:()=>o.showPassword=!o.showPassword},t.i({className:()=>o.showPassword?`ri-eye-off-line`:`ri-eye-line`,ariaHidden:!0}))))),t.div({className:`col-12`},t.button({className:()=>`btn lg block ${o.isSubmitting?`loading`:``}`,disabled:()=>o.isSubmitting},t.span({className:`txt`},`Confirm new email`)))))}export{r as pageConfirmEmailChange};
import{n as e,t as n}from"./index-VdywtJWb.js";function r(r){let i=r.params?.token||``,a=e(i);if(!a.newEmail||!a.collectionId){app.toasts.error(`Invalid or expired email change token.`),window.location.hash=`#/`;return}app.store.title=`Confirm email change`;let o=store({password:``,isSubmitting:!1,isSuccess:!1,showPassword:!1});async function s(){if(o.isSubmitting)return;o.isSubmitting=!0;let e=new n(app.pb.baseURL);try{await e.collection(a.collectionId).confirmEmailChange(i,o.password),o.isSuccess=!0}catch(e){app.checkApiError(e),o.isSuccess=!1}o.isSubmitting=!1}return t.div({pbEvent:`pageConfirmEmailChange`,className:`wrapper sm m-auto p-b-base`},t.header({className:`txt-center m-b-base`},t.img({className:`main-logo`,src:()=>app.store.mainLogo,ariaHidden:!0,alt:`App logo`}),t.h5({className:`m-t-10`},()=>app.store.title)),()=>o.isSuccess?t.div({pbEvent:`confirmEmailChangeAlert`,className:`alert success txt-center`},t.p(null,`The email was successfully changed.`),t.p(null,`You can go back and sign in with your new email address.`)):t.form({pbEvent:`confirmEmailChangeForm`,className:`grid confirm-email-change-form`,onsubmit:e=>{e.preventDefault(),s()}},t.div({className:`col-12`},t.div({className:`content txt-center m-b-sm`},`Type your password to confirm changing your email address to `,t.strong(null,a.newEmail),`:`),t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:`password_confirm`},`Password`),t.input({id:`password_confirm`,name:`password`,required:!0,autofocus:!0,type:()=>o.showPassword?`text`:`password`,value:()=>o.password,oninput:e=>o.password=e.target.value})),t.div({className:`field addon`},t.button({type:`button`,tabIndex:-1,className:`btn sm transparent secondary circle tooltip-right`,ariaLabel:app.attrs.tooltip(()=>o.showPassword?`Hide password`:`Show password`),onclick:()=>o.showPassword=!o.showPassword},t.i({className:()=>o.showPassword?`ri-eye-off-line`:`ri-eye-line`,ariaHidden:!0}))))),t.div({className:`col-12`},t.button({className:()=>`btn lg block ${o.isSubmitting?`loading`:``}`,disabled:()=>o.isSubmitting},t.span({className:`txt`},`Confirm new email`)))))}export{r as pageConfirmEmailChange};
@@ -1 +1 @@
import{n as e,t as n}from"./index-DiLDboS9.js";function r(r){let i=r.params?.token||``,a=e(i);if(!a.email||!a.collectionId){app.toasts.error(`Invalid or expired password reset token.`),window.location.hash=`#/`;return}app.store.title=`Confirm password reset`;let o=store({newPassword:``,newPasswordConfirm:``,showNewPassword:!1,showNewPasswordConfirm:!1,isSubmitting:!1,isSuccess:!1});async function s(){if(o.isSubmitting)return;o.isSubmitting=!0;let e=new n(app.pb.baseURL);try{await e.collection(a.collectionId).confirmPasswordReset(i,o.newPassword,o.newPasswordConfirm),o.isSuccess=!0}catch(e){app.checkApiError(e)}o.isSubmitting=!1}return t.div({pbEvent:`pageConfirmPasswordReset`,className:`wrapper sm m-auto p-b-base`},t.header({className:`txt-center m-b-base`},t.img({className:`main-logo`,src:()=>app.store.mainLogo,ariaHidden:!0,alt:`App logo`}),t.h5({className:`m-t-10`},()=>app.store.title)),()=>o.isSuccess?t.div({pbEvent:`confirmPasswordResetAlert`,className:`alert success txt-center`},t.p(null,`The password was successfully changed.`),t.p(null,`You can go back to sign in with your new password.`)):t.form({pbEvent:`confirmPasswordResetForm`,className:`grid confirm-password-reset-form`,onsubmit:e=>{e.preventDefault(),s()}},t.div({className:`col-12`},t.div({className:`content txt-center m-b-sm`},`Type your new password for `,t.strong(null,a.email),`:`),t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:`newPassword`},`New password`),t.input({id:`newPassword`,name:`password`,required:!0,autofocus:!0,autocomplete:`new-password`,type:()=>o.showNewPassword?`text`:`password`,value:()=>o.newPassword,oninput:e=>o.newPassword=e.target.value})),t.div({className:`field addon`},t.button({type:`button`,tabIndex:-1,className:`btn sm transparent secondary circle tooltip-right`,ariaLabel:app.attrs.tooltip(()=>o.showNewPassword?`Hide password`:`Show password`),onclick:()=>o.showNewPassword=!o.showNewPassword},t.i({className:()=>o.showNewPassword?`ri-eye-off-line`:`ri-eye-line`,ariaHidden:!0}))))),t.div({className:`col-12`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:`newPasswordConfirm`},`New password confirm`),t.input({id:`newPasswordConfirm`,name:`passwordConfirm`,required:!0,autocomplete:`new-password`,type:()=>o.showNewPasswordConfirm?`text`:`password`,value:()=>o.newPasswordConfirm,oninput:e=>o.newPasswordConfirm=e.target.value})),t.div({className:`field addon`},t.button({type:`button`,tabIndex:-1,className:`btn sm transparent secondary circle tooltip-right`,ariaLabel:app.attrs.tooltip(()=>o.showNewPasswordConfirm?`Hide password`:`Show password`),onclick:()=>o.showNewPasswordConfirm=!o.showNewPasswordConfirm},t.i({className:()=>o.showNewPasswordConfirm?`ri-eye-off-line`:`ri-eye-line`,ariaHidden:!0}))))),t.div({className:`col-12`},t.button({className:()=>`btn lg block ${o.isSubmitting?`loading`:``}`,disabled:()=>o.isSubmitting},t.span({className:`txt`},`Set new password`)))))}export{r as pageConfirmPasswordReset};
import{n as e,t as n}from"./index-VdywtJWb.js";function r(r){let i=r.params?.token||``,a=e(i);if(!a.email||!a.collectionId){app.toasts.error(`Invalid or expired password reset token.`),window.location.hash=`#/`;return}app.store.title=`Confirm password reset`;let o=store({newPassword:``,newPasswordConfirm:``,showNewPassword:!1,showNewPasswordConfirm:!1,isSubmitting:!1,isSuccess:!1});async function s(){if(o.isSubmitting)return;o.isSubmitting=!0;let e=new n(app.pb.baseURL);try{await e.collection(a.collectionId).confirmPasswordReset(i,o.newPassword,o.newPasswordConfirm),o.isSuccess=!0}catch(e){app.checkApiError(e)}o.isSubmitting=!1}return t.div({pbEvent:`pageConfirmPasswordReset`,className:`wrapper sm m-auto p-b-base`},t.header({className:`txt-center m-b-base`},t.img({className:`main-logo`,src:()=>app.store.mainLogo,ariaHidden:!0,alt:`App logo`}),t.h5({className:`m-t-10`},()=>app.store.title)),()=>o.isSuccess?t.div({pbEvent:`confirmPasswordResetAlert`,className:`alert success txt-center`},t.p(null,`The password was successfully changed.`),t.p(null,`You can go back to sign in with your new password.`)):t.form({pbEvent:`confirmPasswordResetForm`,className:`grid confirm-password-reset-form`,onsubmit:e=>{e.preventDefault(),s()}},t.div({className:`col-12`},t.div({className:`content txt-center m-b-sm`},`Type your new password for `,t.strong(null,a.email),`:`),t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:`newPassword`},`New password`),t.input({id:`newPassword`,name:`password`,required:!0,autofocus:!0,autocomplete:`new-password`,type:()=>o.showNewPassword?`text`:`password`,value:()=>o.newPassword,oninput:e=>o.newPassword=e.target.value})),t.div({className:`field addon`},t.button({type:`button`,tabIndex:-1,className:`btn sm transparent secondary circle tooltip-right`,ariaLabel:app.attrs.tooltip(()=>o.showNewPassword?`Hide password`:`Show password`),onclick:()=>o.showNewPassword=!o.showNewPassword},t.i({className:()=>o.showNewPassword?`ri-eye-off-line`:`ri-eye-line`,ariaHidden:!0}))))),t.div({className:`col-12`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:`newPasswordConfirm`},`New password confirm`),t.input({id:`newPasswordConfirm`,name:`passwordConfirm`,required:!0,autocomplete:`new-password`,type:()=>o.showNewPasswordConfirm?`text`:`password`,value:()=>o.newPasswordConfirm,oninput:e=>o.newPasswordConfirm=e.target.value})),t.div({className:`field addon`},t.button({type:`button`,tabIndex:-1,className:`btn sm transparent secondary circle tooltip-right`,ariaLabel:app.attrs.tooltip(()=>o.showNewPasswordConfirm?`Hide password`:`Show password`),onclick:()=>o.showNewPasswordConfirm=!o.showNewPasswordConfirm},t.i({className:()=>o.showNewPasswordConfirm?`ri-eye-off-line`:`ri-eye-line`,ariaHidden:!0}))))),t.div({className:`col-12`},t.button({className:()=>`btn lg block ${o.isSubmitting?`loading`:``}`,disabled:()=>o.isSubmitting},t.span({className:`txt`},`Set new password`)))))}export{r as pageConfirmPasswordReset};
@@ -1 +1 @@
import{n as e,t as n}from"./index-DiLDboS9.js";function r(r){let i=r.params?.token||``,a=e(i);if(!a.email||!a.collectionId){app.toasts.error(`Invalid or expired verification token.`),window.location.hash=`#/`;return}app.store.title=`Confirm verification`;let o=store({isConfirming:!1,isConfirmSuccess:!1,isResending:!1,isResendSuccess:!1});s();async function s(){if(o.isConfirming)return;o.isConfirming=!0;let e=new n(app.pb.baseURL);try{await e.collection(a.collectionId).confirmVerification(i),o.isConfirmSuccess=!0}catch{o.isConfirmSuccess=!1}o.isConfirming=!1}async function c(){if(o.isResending)return;o.isResending=!0;let e=new n(`../`);try{await e.collection(a.collectionId).requestVerification(a.email),o.isResendSuccess=!0}catch(e){app.checkApiError(e),o.isResendSuccess=!1}o.isResending=!1}return t.div({pbEvent:`pageConfirmVerification`,className:`wrapper sm m-auto p-b-base`},t.header({className:`txt-center m-b-base`},t.img({className:`main-logo`,src:()=>app.store.mainLogo,ariaHidden:!0,alt:`App logo`}),t.h5({className:`m-t-10`},()=>app.store.title)),()=>o.isConfirming?t.div({className:`block txt-center`},t.span({className:`loader`},`Please wait...`)):o.isConfirmSuccess?t.div({pbEvent:`confirmVerificationSuccessAlert`,className:`alert success txt-center`},t.p(null,`Successfully verified `,t.strong(null,a.email),`.`)):o.isResendSuccess?t.div({pbEvent:`confirmVerificationResendAlert`,className:`alert success txt-center`},t.p(null,`Please check your email for the new verification link.`)):[t.div({pbEvent:`confirmVerificationErrorAlert`,className:`alert danger txt-center m-b-base`},t.p(null,`Invalid or expired verification token.`)),t.button({type:`button`,className:()=>`btn transparent lg block ${o.isResending?`loading`:``}`,disabled:()=>o.isResending,onclick:()=>c()},t.span({className:`txt`},`Resend`))])}export{r as pageConfirmVerification};
import{n as e,t as n}from"./index-VdywtJWb.js";function r(r){let i=r.params?.token||``,a=e(i);if(!a.email||!a.collectionId){app.toasts.error(`Invalid or expired verification token.`),window.location.hash=`#/`;return}app.store.title=`Confirm verification`;let o=store({isConfirming:!1,isConfirmSuccess:!1,isResending:!1,isResendSuccess:!1});s();async function s(){if(o.isConfirming)return;o.isConfirming=!0;let e=new n(app.pb.baseURL);try{await e.collection(a.collectionId).confirmVerification(i),o.isConfirmSuccess=!0}catch{o.isConfirmSuccess=!1}o.isConfirming=!1}async function c(){if(o.isResending)return;o.isResending=!0;let e=new n(`../`);try{await e.collection(a.collectionId).requestVerification(a.email),o.isResendSuccess=!0}catch(e){app.checkApiError(e),o.isResendSuccess=!1}o.isResending=!1}return t.div({pbEvent:`pageConfirmVerification`,className:`wrapper sm m-auto p-b-base`},t.header({className:`txt-center m-b-base`},t.img({className:`main-logo`,src:()=>app.store.mainLogo,ariaHidden:!0,alt:`App logo`}),t.h5({className:`m-t-10`},()=>app.store.title)),()=>o.isConfirming?t.div({className:`block txt-center`},t.span({className:`loader`},`Please wait...`)):o.isConfirmSuccess?t.div({pbEvent:`confirmVerificationSuccessAlert`,className:`alert success txt-center`},t.p(null,`Successfully verified `,t.strong(null,a.email),`.`)):o.isResendSuccess?t.div({pbEvent:`confirmVerificationResendAlert`,className:`alert success txt-center`},t.p(null,`Please check your email for the new verification link.`)):[t.div({pbEvent:`confirmVerificationErrorAlert`,className:`alert danger txt-center m-b-base`},t.p(null,`Invalid or expired verification token.`)),t.button({type:`button`,className:()=>`btn transparent lg block ${o.isResending?`loading`:``}`,disabled:()=>o.isResending,onclick:()=>c()},t.span({className:`txt`},`Resend`))])}export{r as pageConfirmVerification};
@@ -1 +1 @@
import{n as e,r as n}from"./index-DiLDboS9.js";function r(r){let i=r.params?.token||``;if(e(i).type!=`auth`||n(i)){app.toasts.error(`The installer token is invalid or has expired.`),window.location.hash=`#/`;return}app.store.title=`Setup your PocketBase instance`;let a=store({email:``,password:``,passwordConfirm:``,showPassword:!1,showPasswordConfirm:!1,isSubmitting:!1,isUploading:!1,get isBusy(){return a.isSubmitting||a.isUploading}});async function o(){if(!a.isBusy){a.isSubmitting=!0;try{await app.pb.collection(`_superusers`).create({email:a.email,password:a.password,passwordConfirm:a.passwordConfirm},{headers:{Authorization:i}}),await app.pb.collection(`_superusers`).authWithPassword(a.email,a.password),window.location.hash=`#/`}catch(e){app.checkApiError(e)}a.isSubmitting=!1}}let s=`backupFileInput`;function c(){let e=document.getElementById(s);e&&(e.value=``)}function l(e){e&&app.modals.confirm(t.h6(null,`Note that we don't perform validations for the uploaded backup files. Proceed with caution and only if you trust the file source.\n\nDo you really want to upload and initialize "${e.name}"?`),()=>{u(e)},()=>{c()})}async function u(e){if(!(!e||a.isBusy)){a.isUploading=!0;try{await app.pb.backups.upload({file:e},{headers:{Authorization:i}}),await app.pb.backups.restore(e.name,{headers:{Authorization:i}}),app.toasts.info(`Please wait while extracting the uploaded archive!`),await new Promise(e=>setTimeout(e,3e3)),window.location.href=`#/`}catch(e){app.checkApiError(e)}c(),a.isUploading=!1}}return t.div({pbEvent:`pageInstaller`,className:`wrapper sm m-auto p-b-base`},t.header({className:`txt-center m-b-base`},t.img({className:`main-logo`,src:()=>app.store.mainLogo,ariaHidden:!0,alt:`App logo`}),t.h5({className:`m-t-10`},()=>app.store.title)),t.form({pbEvent:`installerForm`,className:`grid installer-form`,onsubmit:e=>{e.preventDefault(),o(a)}},t.div({className:`col-12 txt-center`},`Create your first superuser account in order to continue:`),t.div({className:`col-12`},t.div({className:`field`},t.label({htmlFor:`superuser_email`},`Email`),t.input({id:`superuser_email`,name:`email`,type:`email`,required:!0,autofocus:!0,autocomplete:`off`,disabled:()=>a.isBusy,value:()=>a.email,oninput:e=>a.email=e.target.value}))),t.div({className:`col-12`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:`superuser_password`},`Password`),t.input({id:`superuser_password`,name:`password`,min:10,required:!0,disabled:()=>a.isBusy,type:()=>a.showPassword?`text`:`password`,value:()=>a.password,oninput:e=>a.password=e.target.value})),t.div({className:`field addon`},t.button({type:`button`,tabIndex:-1,className:`btn sm transparent secondary circle tooltip-right`,ariaLabel:app.attrs.tooltip(()=>a.showPassword?`Hide password`:`Show password`),onclick:()=>a.showPassword=!a.showPassword},t.i({className:()=>a.showPassword?`ri-eye-off-line`:`ri-eye-line`,ariaHidden:!0})))),t.div({className:`field-help`},`Recommended at least 10 characters.`)),t.div({className:`col-12`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:`superuser_password_confirm`},`Password confirm`),t.input({id:`superuser_password_confirm`,name:`passwordConfirm`,required:!0,disabled:()=>a.isBusy,type:()=>a.showPasswordConfirm?`text`:`password`,value:()=>a.passwordConfirm,oninput:e=>a.passwordConfirm=e.target.value})),t.div({className:`field addon`},t.button({type:`button`,tabIndex:-1,className:`btn sm transparent secondary circle tooltip-right`,ariaLabel:app.attrs.tooltip(()=>a.showPasswordConfirm?`Hide password`:`Show password`),onclick:()=>a.showPasswordConfirm=!a.showPasswordConfirm},t.i({className:()=>a.showPasswordConfirm?`ri-eye-off-line`:`ri-eye-line`,ariaHidden:!0}))))),t.div({className:`col-12`},t.button({className:()=>`btn lg next block ${a.isSubmitting?`loading`:``}`,disabled:()=>a.isBusy},t.span({className:`txt`},`Create superuser and login`),t.i({className:`ri-arrow-right-line`,ariaHidden:!0})))),t.hr(),t.label({htmlFor:s,className:()=>`btn secondary transparent lg block ${a.isBusy?`disabled`:``} ${a.isUploading?`loading`:``}`},t.i({className:`ri-upload-cloud-line`,ariaHidden:!0}),t.span({className:`txt`},`Or initialize from backup`)),t.input({id:s,type:`file`,className:`hidden`,accept:`.zip`,onchange:e=>{l(e.target?.files?.[0])}}))}export{r as pageInstaller};
import{n as e,r as n}from"./index-VdywtJWb.js";function r(r){let i=r.params?.token||``;if(e(i).type!=`auth`||n(i)){app.toasts.error(`The installer token is invalid or has expired.`),window.location.hash=`#/`;return}app.store.title=`Setup your PocketBase instance`;let a=store({email:``,password:``,passwordConfirm:``,showPassword:!1,showPasswordConfirm:!1,isSubmitting:!1,isUploading:!1,get isBusy(){return a.isSubmitting||a.isUploading}});async function o(){if(!a.isBusy){a.isSubmitting=!0;try{await app.pb.collection(`_superusers`).create({email:a.email,password:a.password,passwordConfirm:a.passwordConfirm},{headers:{Authorization:i}}),await app.pb.collection(`_superusers`).authWithPassword(a.email,a.password),window.location.hash=`#/`}catch(e){app.checkApiError(e)}a.isSubmitting=!1}}let s=`backupFileInput`;function c(){let e=document.getElementById(s);e&&(e.value=``)}function l(e){e&&app.modals.confirm(t.h6(null,`Note that we don't perform validations for the uploaded backup files. Proceed with caution and only if you trust the file source.\n\nDo you really want to upload and initialize "${e.name}"?`),()=>{u(e)},()=>{c()})}async function u(e){if(!(!e||a.isBusy)){a.isUploading=!0;try{await app.pb.backups.upload({file:e},{headers:{Authorization:i}}),await app.pb.backups.restore(e.name,{headers:{Authorization:i}}),app.toasts.info(`Please wait while extracting the uploaded archive!`),await new Promise(e=>setTimeout(e,3e3)),window.location.href=`#/`}catch(e){app.checkApiError(e)}c(),a.isUploading=!1}}return t.div({pbEvent:`pageInstaller`,className:`wrapper sm m-auto p-b-base`},t.header({className:`txt-center m-b-base`},t.img({className:`main-logo`,src:()=>app.store.mainLogo,ariaHidden:!0,alt:`App logo`}),t.h5({className:`m-t-10`},()=>app.store.title)),t.form({pbEvent:`installerForm`,className:`grid installer-form`,onsubmit:e=>{e.preventDefault(),o(a)}},t.div({className:`col-12 txt-center`},`Create your first superuser account in order to continue:`),t.div({className:`col-12`},t.div({className:`field`},t.label({htmlFor:`superuser_email`},`Email`),t.input({id:`superuser_email`,name:`email`,type:`email`,required:!0,autofocus:!0,autocomplete:`off`,disabled:()=>a.isBusy,value:()=>a.email,oninput:e=>a.email=e.target.value}))),t.div({className:`col-12`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:`superuser_password`},`Password`),t.input({id:`superuser_password`,name:`password`,min:10,required:!0,disabled:()=>a.isBusy,type:()=>a.showPassword?`text`:`password`,value:()=>a.password,oninput:e=>a.password=e.target.value})),t.div({className:`field addon`},t.button({type:`button`,tabIndex:-1,className:`btn sm transparent secondary circle tooltip-right`,ariaLabel:app.attrs.tooltip(()=>a.showPassword?`Hide password`:`Show password`),onclick:()=>a.showPassword=!a.showPassword},t.i({className:()=>a.showPassword?`ri-eye-off-line`:`ri-eye-line`,ariaHidden:!0})))),t.div({className:`field-help`},`Recommended at least 10 characters.`)),t.div({className:`col-12`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:`superuser_password_confirm`},`Password confirm`),t.input({id:`superuser_password_confirm`,name:`passwordConfirm`,required:!0,disabled:()=>a.isBusy,type:()=>a.showPasswordConfirm?`text`:`password`,value:()=>a.passwordConfirm,oninput:e=>a.passwordConfirm=e.target.value})),t.div({className:`field addon`},t.button({type:`button`,tabIndex:-1,className:`btn sm transparent secondary circle tooltip-right`,ariaLabel:app.attrs.tooltip(()=>a.showPasswordConfirm?`Hide password`:`Show password`),onclick:()=>a.showPasswordConfirm=!a.showPasswordConfirm},t.i({className:()=>a.showPasswordConfirm?`ri-eye-off-line`:`ri-eye-line`,ariaHidden:!0}))))),t.div({className:`col-12`},t.button({className:()=>`btn lg next block ${a.isSubmitting?`loading`:``}`,disabled:()=>a.isBusy},t.span({className:`txt`},`Create superuser and login`),t.i({className:`ri-arrow-right-line`,ariaHidden:!0})))),t.hr(),t.label({htmlFor:s,className:()=>`btn secondary transparent lg block ${a.isBusy?`disabled`:``} ${a.isUploading?`loading`:``}`},t.i({className:`ri-upload-cloud-line`,ariaHidden:!0}),t.span({className:`txt`},`Or initialize from backup`)),t.input({id:s,type:`file`,className:`hidden`,accept:`.zip`,onchange:e=>{l(e.target?.files?.[0])}}))}export{r as pageInstaller};
+2 -2
View File
@@ -13,8 +13,8 @@
<!-- prism -->
<script src="./libs/prism/prism.js" data-manual></script>
<script type="module" crossorigin src="./assets/index-DiLDboS9.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-CZinEv5W.css">
<script type="module" crossorigin src="./assets/index-VdywtJWb.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-kt0ysE3i.css">
</head>
<body>
</body>
+74 -57
View File
@@ -108,6 +108,8 @@ window.app.components.codeEditor = function(propsArg = {}) {
}
function closeAutocompleteDropdown() {
clearTimeout(autocompleteTimeoutId);
if (dropdown) {
dropdown.remove();
dropdown = null;
@@ -131,6 +133,8 @@ window.app.components.codeEditor = function(propsArg = {}) {
let valueWatcher;
let autocompleteTimeoutId;
// note1: use contenteditable so that we can call getBoundingClientRect on the selected text
// note2: getSelection also doesn't seem to work in Firefox for textarea and inputs
const editorContent = t.div({
@@ -173,86 +177,98 @@ window.app.components.codeEditor = function(propsArg = {}) {
props.onblur?.(props.value);
},
oninput: (e) => {
closeAutocompleteDropdown();
clearTimeout(autocompleteTimeoutId);
updateValue(editorContent.textContent);
if (!props.value?.length) {
editorContent.textContent = ""; // ensure that no comments, br, etc. tags are left
closeAutocompleteDropdown();
return;
}
if (!editorContent?.isConnected) {
return;
}
autocompleteTimeoutId = setTimeout(() => {
closeAutocompleteDropdown();
const pos = getCaretPos(editorContent);
if (!editorContent?.isConnected) {
return;
}
const match = getWord(props.value, pos);
const pos = getCaretPos(editorContent);
if (
!match.word.length
// don't show suggestions in case the cursor is at the
// beginning of an already typed word
|| pos == match.start
) {
return;
}
const match = getWord(props.value, pos);
let suggestions = [];
if (typeof props.autocomplete == "function") {
suggestions = props.autocomplete(match.word) || [];
} else if (!app.utils.isEmpty(props.autocomplete)) {
const wordLowercased = match.word.toLowerCase();
suggestions = props.autocomplete.filter((item) => {
if (typeof item == "object") {
item = item?.value;
}
if (
!match.word.length
// don't show suggestions in case the cursor is at the
// beginning of an already typed word
|| pos == match.start
) {
return;
}
item = item?.toLowerCase();
let suggestions = [];
if (typeof props.autocomplete == "function") {
suggestions = props.autocomplete(match.prefix || match.word) || [];
} else if (!app.utils.isEmpty(props.autocomplete)) {
const wordLowercased = (match.prefix || match.word).toLowerCase();
suggestions = props.autocomplete.filter((item) => {
if (typeof item == "object") {
item = item?.value;
}
return item && item != wordLowercased && item.includes(wordLowercased);
});
}
item = item?.toLowerCase();
if (!suggestions?.length) {
return;
}
return item && item != wordLowercased && item.includes(wordLowercased);
});
}
openAutocompleteDropdown(() => {
return suggestions.map((suggestion, i) => {
return t.button({
type: "button",
className: `dropdown-item ${i == 0 ? "active" : ""}`,
textContent: suggestion.label || suggestion.value || suggestion,
onclick: (e) => {
e.preventDefault();
if (
!suggestions?.length
// don't show if the only suggestion is exact match
|| (
suggestions.length == 1
&& (suggestions[0].value || suggestions[0]) == match.word
)
) {
return;
}
editorContent.focus();
openAutocompleteDropdown(() => {
return suggestions.map((suggestion, i) => {
return t.button({
type: "button",
className: `dropdown-item ${i == 0 ? "active" : ""}`,
textContent: suggestion.label || suggestion.value || suggestion,
onclick: (e) => {
e.preventDefault();
const word = suggestion.value || suggestion;
editorContent.focus();
// note: replacing the text doesn't preserve the native "undo" history
// (document.execCommand is being deprecated)
editorContent.textContent = editorContent.textContent.substring(0, match.start)
+ word
+ editorContent.textContent.substring(match.end + 1);
const word = suggestion.value || suggestion;
updateValue(editorContent.textContent);
// note: replacing the text doesn't preserve the native "undo" history
// (document.execCommand is being deprecated)
editorContent.textContent = editorContent.textContent.substring(0, match.start)
+ word
+ editorContent.textContent.substring(match.end + 1);
try {
window
.getSelection()
.setPosition(editorContent.childNodes[0], match.start + word.length);
} catch (err) {
console.warn("failed to set caret position", err);
}
updateValue(editorContent.textContent);
closeAutocompleteDropdown();
},
try {
window
.getSelection()
.setPosition(editorContent.childNodes[0], match.start + word.length);
} catch (err) {
console.warn("failed to set caret position", err);
}
closeAutocompleteDropdown();
},
});
});
});
});
}, 50);
},
onkeydown: (e) => {
isCtrlOrCmdKey = e.ctrlKey || e.metaKey;
@@ -404,7 +420,7 @@ window.app.components.codeEditor = function(propsArg = {}) {
);
};
const highlightThreshold = 500;
const highlightThreshold = 800;
function highlight(content, language) {
content = typeof content == "string" ? content : "";
@@ -444,6 +460,7 @@ function getWord(value, caretPos) {
return {
word: value.substring(start, end + 1),
prefix: value.substring(start, caretPos),
start: start,
end: end,
};
+1 -1
View File
@@ -155,7 +155,7 @@
--secondaryAlt1Color: var(--surfaceAlt3Color);
--secondaryAlt2Color: var(--surfaceAlt4Color);
--primaryColor: #121212;
--primaryColor: var(--surfaceAlt4Color);
--primaryTxtColor: var(--surfaceTxtColor);
--primaryAlt1Color: color-mix(in srgb, var(--primaryColor), black 12%);
--primaryAlt2Color: color-mix(in srgb, var(--primaryColor), black 25%);
+1 -1
View File
@@ -129,7 +129,7 @@ app.pb.authStore.onChange((_, record) => {
/**
* Return a superuser file token.
* Optionally you can provide a collection identifier, to avoid unnecessery
* Optionally you can provide a collection identifier, to avoid unnecessary
* calls in case the collection doesn't have protected files.
*
* @param {String} optCollectionIdORName
+9 -1
View File
@@ -153,6 +153,7 @@ window.app.components.recordsList = function(propsArg = {}) {
});
if (result.page == 1) {
props.suggestReset = false;
clearList();
}
@@ -318,7 +319,14 @@ window.app.components.recordsList = function(propsArg = {}) {
clearTimeout(deleteRefreshTimeoutId);
deleteRefreshTimeoutId = setTimeout(() => {
if (!data.records?.length) {
if (
// no more client-side records
!data.records?.length
// has cascade self-reference field
|| !!props.collection?.fields?.find((f) => {
return f.type == "relation" && f.cascadeDelete && f.collectionId == props.collection.id;
})
) {
loadRecords(true);
}
}, 100);
+4 -4
View File
@@ -1667,14 +1667,14 @@ const utils = {
};
const indexRegex =
/create\s+(unique\s+)?\s*index\s*(if\s+not\s+exists\s+)?(\S*)\s+on\s+(\S*)\s*\(([\s\S]*)\)(?:\s*where\s+([\s\S]*))?/gim;
/\s*create\s+(unique\s+)?\s*index\s*(if\s+not\s+exists\s+)?(\S*)\s+on\s+(\S*)\s*\(([\s\S]*?)\)(?:\s*where\s+([\s\S]*?))?\s*$/gi;
const matches = indexRegex.exec((idx || "").trim());
if (matches?.length != 7) {
return result;
}
const sqlQuoteRegex = /^[\"\'\`\[\{}]|[\"\'\`\]\}]$/gm;
const sqlQuoteRegex = /^[\"\'\`\[\{}]|[\"\'\`\]\}]$/g;
// unique
result.unique = matches[1]?.trim().toLowerCase() === "unique";
@@ -1697,13 +1697,13 @@ const utils = {
// columns
const rawColumns = (matches[5] || "")
.replace(/,(?=[^\(]*\))/gim, "{PB_TEMP}") // temporary replace comma within expressions for easier splitting
.replace(/,(?=[^\(]*\))/gi, "{PB_TEMP}") // temporary replace comma within expressions for easier splitting
.split(","); // split columns
for (let col of rawColumns) {
col = col.trim().replaceAll("{PB_TEMP}", ","); // revert temp replacement
const colRegex = /^([\s\S]+?)(?:\s+collate\s+([\w]+))?(?:\s+(asc|desc))?$/gim;
const colRegex = /^([\s\S]+?)(?:\s+collate\s+([\w]+))?(?:\s+(asc|desc))?\s*$/gi;
const colMatches = colRegex.exec(col);
if (colMatches?.length != 4) {
continue;