minimal sql console API and UI
This commit is contained in:
@@ -48,6 +48,7 @@ func NewRouter(app core.App) (*router.Router[*core.RequestEvent], error) {
|
|||||||
bindBatchApi(app, apiGroup)
|
bindBatchApi(app, apiGroup)
|
||||||
bindRealtimeApi(app, apiGroup)
|
bindRealtimeApi(app, apiGroup)
|
||||||
bindHealthApi(app, apiGroup)
|
bindHealthApi(app, apiGroup)
|
||||||
|
bindSQLApi(app, apiGroup)
|
||||||
|
|
||||||
// UI routes
|
// UI routes
|
||||||
bindUIExtensions(app)
|
bindUIExtensions(app)
|
||||||
|
|||||||
+191
@@ -0,0 +1,191 @@
|
|||||||
|
package apis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
runSQLMaxRows = 1000
|
||||||
|
runSQLMaxTimeout = 3 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
// bindSQLApi registers the SQL api endpoints.
|
||||||
|
func bindSQLApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) {
|
||||||
|
subGroup := rg.Group("/sql").Bind(RequireSuperuserAuth())
|
||||||
|
subGroup.POST("", runSQL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSQL(e *core.RequestEvent) error {
|
||||||
|
// extra precaution in case manually invoked from somewhere else
|
||||||
|
if !e.HasSuperuserAuth() {
|
||||||
|
return e.ForbiddenError("", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
form := runSQLForm{}
|
||||||
|
|
||||||
|
err := e.BindBody(&form)
|
||||||
|
if err != nil {
|
||||||
|
return firstApiError(err, e.BadRequestError("An error occurred while loading the submitted data.", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = form.validate()
|
||||||
|
if err != nil {
|
||||||
|
return firstApiError(err, e.BadRequestError("An error occurred while validating the submitted data.", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := executeQuery(e.App, form.Query, runSQLMaxRows)
|
||||||
|
if err != nil {
|
||||||
|
return firstApiError(err, e.BadRequestError("Failed to execute query. Raw error:\n"+err.Error(), nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
type runSQLForm struct {
|
||||||
|
Query string `form:"query" json:"query"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (form *runSQLForm) validate() error {
|
||||||
|
return validation.ValidateStruct(form,
|
||||||
|
validation.Field(&form.Query, validation.Required, validation.Length(0, 3000)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type runSQLResultColumn struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Nullable bool `json:"nullable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type runSQLResult struct {
|
||||||
|
ExecTime int64 `json:"execTime"`
|
||||||
|
AffectedRows int64 `json:"affectedRows"`
|
||||||
|
Columns []runSQLResultColumn `json:"columns"`
|
||||||
|
Rows [][]any `json:"rows"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var knownWriteQueryPrefixes = []string{"INSERT", "CREATE", "UPDATE", "DELETE", "DROP", "DETACH"}
|
||||||
|
|
||||||
|
func executeQuery(app core.App, query string, maxRows int) (*runSQLResult, error) {
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
if query == "" {
|
||||||
|
// see https://github.com/mattn/go-sqlite3/issues/950
|
||||||
|
return nil, errors.New("empty query")
|
||||||
|
}
|
||||||
|
|
||||||
|
var isPossibleWriteQuery bool
|
||||||
|
|
||||||
|
// loosely check the query type
|
||||||
|
ucQuery := strings.ToUpper(query)
|
||||||
|
if !strings.HasPrefix(ucQuery, "SELECT") {
|
||||||
|
for _, prefix := range knownWriteQueryPrefixes {
|
||||||
|
if strings.HasPrefix(ucQuery, prefix) {
|
||||||
|
isPossibleWriteQuery = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// note: don't extend the request context to minimize the risk of
|
||||||
|
// causing integrity issues with custom non-transaction mutations
|
||||||
|
ctx, cancelFunc := context.WithTimeout(context.Background(), runSQLMaxTimeout)
|
||||||
|
defer cancelFunc()
|
||||||
|
|
||||||
|
result := &runSQLResult{
|
||||||
|
// init empty slices to ensure "[]" serialization
|
||||||
|
Columns: []runSQLResultColumn{},
|
||||||
|
Rows: [][]any{},
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
defer func() {
|
||||||
|
result.ExecTime = time.Since(now).Milliseconds()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// assume write/mutation query
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
if isPossibleWriteQuery {
|
||||||
|
// auto wrap in transaction in case there are multiple inline queries
|
||||||
|
txErr := app.RunInTransaction(func(txApp core.App) error {
|
||||||
|
execResult, err := txApp.NonconcurrentDB().NewQuery(query).WithContext(ctx).Execute()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.AffectedRows, err = execResult.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
// non-critical error (e.g. not supported by the driver)
|
||||||
|
txApp.Logger().Debug("Unable to fetch affected rows", slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if txErr != nil {
|
||||||
|
return nil, txErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assume query returning rows
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
rows, err := app.ConcurrentDB().NewQuery(query).WithContext(ctx).Rows()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
// populate columns info
|
||||||
|
// ---
|
||||||
|
colTypes, err := rows.ColumnTypes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, colType := range colTypes {
|
||||||
|
col := runSQLResultColumn{
|
||||||
|
Name: colType.Name(),
|
||||||
|
Type: colType.DatabaseTypeName(),
|
||||||
|
}
|
||||||
|
col.Nullable, _ = colType.Nullable()
|
||||||
|
|
||||||
|
result.Columns = append(result.Columns, col)
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate rows
|
||||||
|
// ---
|
||||||
|
for rows.Next() {
|
||||||
|
if len(result.Rows) >= maxRows {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
rowData := make([]any, len(colTypes))
|
||||||
|
for i := 0; i < len(colTypes); i++ {
|
||||||
|
var v *string
|
||||||
|
rowData[i] = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
err := rows.Scan(rowData...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Rows = append(result.Rows, rowData)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package apis_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/tests"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSQLRun(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
scenarios := []tests.ApiScenario{
|
||||||
|
{
|
||||||
|
Name: "guest",
|
||||||
|
Method: http.MethodPost,
|
||||||
|
URL: "/api/sql",
|
||||||
|
Body: strings.NewReader(`{"query":"select 1"}`),
|
||||||
|
ExpectedStatus: 401,
|
||||||
|
ExpectedContent: []string{`"data":{}`},
|
||||||
|
ExpectedEvents: map[string]int{"*": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "regular user",
|
||||||
|
Method: http.MethodPost,
|
||||||
|
URL: "/api/sql",
|
||||||
|
Body: strings.NewReader(`{"query":"select 1"}`),
|
||||||
|
Headers: map[string]string{
|
||||||
|
// users, test2@example.com
|
||||||
|
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6Im9hcDY0MGNvdDR5cnUycyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.GfJo6EHIobgas_AXt-M-tj5IoQendPnrkMSe9ExuSEY",
|
||||||
|
},
|
||||||
|
ExpectedStatus: 403,
|
||||||
|
ExpectedContent: []string{`"data":{}`},
|
||||||
|
ExpectedEvents: map[string]int{"*": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "superuser",
|
||||||
|
Method: http.MethodPost,
|
||||||
|
URL: "/api/sql",
|
||||||
|
Body: strings.NewReader(`{"query":"select 1"}`),
|
||||||
|
Headers: map[string]string{
|
||||||
|
// superusers, test@example.com
|
||||||
|
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||||
|
},
|
||||||
|
ExpectedStatus: 200,
|
||||||
|
ExpectedContent: []string{
|
||||||
|
`"execTime":`,
|
||||||
|
`"affectedRows":0`,
|
||||||
|
`"columns":[{"name":"1","type":"","nullable":true}]`,
|
||||||
|
`"rows":[["1"]]`,
|
||||||
|
},
|
||||||
|
ExpectedEvents: map[string]int{"*": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "empty query",
|
||||||
|
Method: http.MethodPost,
|
||||||
|
URL: "/api/sql",
|
||||||
|
Body: strings.NewReader(`{"query":""}`),
|
||||||
|
Headers: map[string]string{
|
||||||
|
// superusers, test@example.com
|
||||||
|
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||||
|
},
|
||||||
|
ExpectedStatus: 400,
|
||||||
|
ExpectedContent: []string{
|
||||||
|
`"data":{`,
|
||||||
|
`"query":{`,
|
||||||
|
},
|
||||||
|
ExpectedEvents: map[string]int{"*": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "long query",
|
||||||
|
Method: http.MethodPost,
|
||||||
|
URL: "/api/sql",
|
||||||
|
Body: strings.NewReader(`{"query":"` + strings.Repeat("a", 3001) + `"}`),
|
||||||
|
Headers: map[string]string{
|
||||||
|
// superusers, test@example.com
|
||||||
|
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||||
|
},
|
||||||
|
ExpectedStatus: 400,
|
||||||
|
ExpectedContent: []string{
|
||||||
|
`"data":{`,
|
||||||
|
`"query":{`,
|
||||||
|
},
|
||||||
|
ExpectedEvents: map[string]int{"*": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "invalid query",
|
||||||
|
Method: http.MethodPost,
|
||||||
|
URL: "/api/sql",
|
||||||
|
Body: strings.NewReader(`{"query":"invalid"}`),
|
||||||
|
Headers: map[string]string{
|
||||||
|
// superusers, test@example.com
|
||||||
|
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||||
|
},
|
||||||
|
ExpectedStatus: 400,
|
||||||
|
ExpectedContent: []string{
|
||||||
|
`"data":{}`,
|
||||||
|
`Raw error:`,
|
||||||
|
`SQL logic error`,
|
||||||
|
},
|
||||||
|
ExpectedEvents: map[string]int{"*": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "single write query",
|
||||||
|
Method: http.MethodPost,
|
||||||
|
URL: "/api/sql",
|
||||||
|
Body: strings.NewReader(`{"query":"create table test_sql_table(id int primary key)"}`),
|
||||||
|
Headers: map[string]string{
|
||||||
|
// superusers, test@example.com
|
||||||
|
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||||
|
},
|
||||||
|
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
||||||
|
if !app.HasTable("test_sql_table") {
|
||||||
|
t.Fatalf("Missing expected new %q table", "test_sql_table")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ExpectedStatus: 200,
|
||||||
|
ExpectedContent: []string{
|
||||||
|
`"execTime":`,
|
||||||
|
`"affectedRows":0`,
|
||||||
|
`"columns":[]`,
|
||||||
|
`"rows":[]`,
|
||||||
|
},
|
||||||
|
ExpectedEvents: map[string]int{"*": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "multiple write queries",
|
||||||
|
Method: http.MethodPost,
|
||||||
|
URL: "/api/sql",
|
||||||
|
Body: strings.NewReader(`{"query":"create table test_sql_table(id int primary key);insert into test_sql_table(id)VALUES(1)"}`),
|
||||||
|
Headers: map[string]string{
|
||||||
|
// superusers, test@example.com
|
||||||
|
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||||
|
},
|
||||||
|
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
||||||
|
var total int
|
||||||
|
err := app.DB().NewQuery("select count(*) from test_sql_table").Row(&total)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if total != 1 {
|
||||||
|
t.Fatalf("Expected exactly 1 row, found: %d", total)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ExpectedStatus: 200,
|
||||||
|
ExpectedContent: []string{
|
||||||
|
`"execTime":`,
|
||||||
|
`"affectedRows":1`,
|
||||||
|
`"columns":[]`,
|
||||||
|
`"rows":[]`,
|
||||||
|
},
|
||||||
|
ExpectedEvents: map[string]int{"*": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "multiple write queries (transaction rollback)",
|
||||||
|
Method: http.MethodPost,
|
||||||
|
URL: "/api/sql",
|
||||||
|
Body: strings.NewReader(`{"query":"create table test_sql_table(id int primary key);insert into test_sql_table(id)VALUES(1);invalid"}`),
|
||||||
|
Headers: map[string]string{
|
||||||
|
// superusers, test@example.com
|
||||||
|
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||||
|
},
|
||||||
|
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
||||||
|
if app.HasTable("test_sql_table") {
|
||||||
|
t.Fatalf("Expected table %q to not be created", "test_sql_table")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ExpectedStatus: 400,
|
||||||
|
ExpectedContent: []string{
|
||||||
|
`"data":{}`,
|
||||||
|
`Raw error:`,
|
||||||
|
`SQL logic error`,
|
||||||
|
},
|
||||||
|
ExpectedEvents: map[string]int{"*": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "multiple read queries",
|
||||||
|
Method: http.MethodPost,
|
||||||
|
URL: "/api/sql",
|
||||||
|
Body: strings.NewReader(`{"query":"select 1;select 2"}`),
|
||||||
|
Headers: map[string]string{
|
||||||
|
// superusers, test@example.com
|
||||||
|
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||||
|
},
|
||||||
|
ExpectedStatus: 200,
|
||||||
|
ExpectedContent: []string{
|
||||||
|
`"execTime":`,
|
||||||
|
`"affectedRows":0`,
|
||||||
|
// only the result of the last query should be returned
|
||||||
|
`"columns":[{"name":"2","type":"","nullable":true}]`,
|
||||||
|
`"rows":[["2"]]`,
|
||||||
|
},
|
||||||
|
ExpectedEvents: map[string]int{"*": 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
scenario.Test(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -13,9 +13,9 @@
|
|||||||
|
|
||||||
<!-- prism -->
|
<!-- prism -->
|
||||||
<script src="./libs/prism/prism.js" data-manual></script>
|
<script src="./libs/prism/prism.js" data-manual></script>
|
||||||
<script type="module" crossorigin src="./assets/index-1u9sxa_M.js"></script>
|
<script type="module" crossorigin src="./assets/index-BRepvmcR.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="./assets/pocketbase.es-B_4DUNUU.js">
|
<link rel="modulepreload" crossorigin href="./assets/pocketbase.es-B_4DUNUU.js">
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-ePwHvxbV.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-C5MjNoft.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
(function(e){let t,n=new Set,r=new Map,i=[],a,o=Symbol(),s=Symbol(),c=Symbol(),l=Symbol(),u=Symbol(),d=Symbol(),f=Symbol(),p=Symbol(),m=Symbol(),h=Symbol();function g(e,n){let d={[o]:`_`+Math.random(),[p]:e,[m]:n};return r.set(d[o],d),d.run=()=>{let e;t&&(e=t,d[s]=t[o],t[c]=t[c]||[],t[c].push(d[o])),d[l]?.forEach(e=>{e.delete(d[o])}),t=d;let n=d[p](d[h],d);d[m]&&(t=null,d[m](n,d[h],d)),d[h]=n,t=e},d.unwatch=function(){d[u]=1,i.push(d[o]),a&&clearTimeout(a),a=setTimeout(()=>{for(let e of i)_(e);i=[],a=null},50)},d.run(),d}function _(e){let t=r.get(e);if(t?.[d]?.(),t?.[c]){for(let e of t[c])_(e);t[s]=null,t[c]=null}if(t?.[l]){for(let n of t[l])n.delete(e);t[l]=null}r.delete(e)}function v(e){return y(e,new Map)}function y(e,n){let r=typeof e==`object`&&!Array.isArray(e)?Object.getOwnPropertyDescriptors(e):{};return new Proxy(e,{get(e,i,a){if(typeof i==`symbol`)return e[i];if(i==`__raw`)return e;if(r[i]?.get&&!e[s]){let n=i;if(i=`@@`+i,!t){let t=r[n].get.call(e);return r[n]._watcher&&(a[i]=t),t}let s=t[o];if(r[n]._refs=r[n]._refs||new Set,!r[n]._refs.has(s)){r[n]._refs.add(s);let e=t[d];t[d]=()=>{e?.(),r[n]._refs.delete(s),!r[n]._refs.size&&r[n]._watcher&&_(r[n]._watcher[o])}}if(!r[n]._watcher){let o=t;t=null;let s=g(r[n].get.bind(e),t=>{e.hasOwnProperty(i)?a[i]=t:Object.defineProperty(e,i,{writable:!0,enumerable:!1,value:t})});r[n]._watcher=s,s[d]=()=>{r[n]._watcher=null},t=o}}let c=e?.[i];if(typeof c==`function`)return c;if(typeof c==`object`&&c&&!c[s]&&(c.constructor?.name==`Object`||c.constructor?.name==`Array`||c.constructor?.name==null)&&(c[s]=[a,i],c=y(c,n),e[i]=c),t){let r=b(e,i),a=t[o],s=[r];t[l]=t[l]||new Set;for(let e of s){let r=n.get(e);r||(r=new Set,n.set(e,r)),r.add(a),t[l].add(r)}}return c},set(e,t,r){if(typeof t==`symbol`)return e[t]=r,!0;let i=e[t];i?.[s]&&(i[f]=!0),r?.[s]&&Array.isArray(e)&&!isNaN(t)&&(r[s][1]=t);let a=!1;return i===void 0&&!e.hasOwnProperty(t)&&(a=!0),e[t]=r,a&&x(e,`toJSON`,n),(r!==i||t===`length`)&&x(e,t,n),!0},deleteProperty(e,t){if(typeof t!=`symbol`){x(e,t,n);let r=b(e,t);for(let e of n)(e[0]==r||e[0].startsWith(r+`/`))&&n.delete(e[0])}return delete e[t]}})}function b(e,t){let n=t,r=e?.[s];for(;r;)n=r[1]+`/`+n,r=r[0][s];return n}function x(e,t,i){let a=b(e,t),o=i.get(a);if(o)for(let e of o)n.delete(e),n.add(e),n.size==1&&queueMicrotask(()=>{let e={},t;for(let i of n)if(t=r.get(i),!(!t||t[u])&&!(t[s]&&n.has(t[s]))){if(e[i]=(e[i]||0)+1,e[i]>250){console.warn(`Possible infinite loop for watcher `+i+`:`,`
|
(function(e){let t,n=new Set,r=new Map,i=[],a,o=Symbol(),s=Symbol(),c=Symbol(),l=Symbol(),u=Symbol(),d=Symbol(),f=Symbol(),p=Symbol(),m=Symbol(),h=Symbol();function g(e,n){let d={[o]:`_`+Math.random(),[p]:e,[m]:n};return r.set(d[o],d),d.run=()=>{let e;t&&(e=t,d[s]=t[o],t[c]=t[c]||[],t[c].push(d[o])),d[l]?.forEach(e=>{e.delete(d[o])}),t=d;let n=d[p](d[h],d);d[m]&&(t=null,d[m](n,d[h],d)),d[h]=n,t=e},d.unwatch=function(){d[u]=1,i.push(d[o]),a&&clearTimeout(a),a=setTimeout(()=>{for(let e of i)_(e);i=[],a=null},50)},d.run(),d}function _(e){let t=r.get(e);if(t?.[d]?.(),t?.[c]){for(let e of t[c])_(e);t[s]=null,t[c]=null}if(t?.[l]){for(let n of t[l])n.delete(e);t[l]=null}r.delete(e)}function v(e){return y(e,new Map)}function y(e,n){let r=typeof e==`object`&&!Array.isArray(e)?Object.getOwnPropertyDescriptors(e):{};return new Proxy(e,{get(e,i,a){if(typeof i==`symbol`)return e[i];if(i==`__raw`)return e;if(r[i]?.get&&!e[s]){let n=i;if(i=`@@`+i,!t){let t=r[n].get.call(e);return r[n]._watcher&&(a[i]=t),t}let s=t[o];if(r[n]._refs=r[n]._refs||new Set,!r[n]._refs.has(s)){r[n]._refs.add(s);let e=t[d];t[d]=()=>{e?.(),r[n]._refs.delete(s),!r[n]._refs.size&&r[n]._watcher&&_(r[n]._watcher[o])}}if(!r[n]._watcher){let o=t;t=null;let s=g(r[n].get.bind(e),t=>{e.hasOwnProperty(i)?a[i]=t:Object.defineProperty(e,i,{writable:!0,enumerable:!1,value:t})});r[n]._watcher=s,s[d]=()=>{r[n]._watcher=null},t=o}}let c=e?.[i];if(typeof c==`function`)return c;if(typeof c==`object`&&c&&!c[s]&&(c.constructor?.name==`Object`||c.constructor?.name==`Array`||c.constructor?.name==null)&&(c[s]=[a,i],c=y(c,n),e[i]=c),t){let r=b(e,i),a=t[o],s=[r];t[l]=t[l]||new Set;for(let e of s){let r=n.get(e);r||(r=new Set,n.set(e,r)),r.add(a),t[l].add(r)}}return c},set(e,t,r){if(typeof t==`symbol`)return e[t]=r,!0;let i=e[t];i?.[s]&&(i[f]=!0),r?.[s]&&Array.isArray(e)&&!isNaN(t)&&(r[s][1]=t);let a=!1;return i===void 0&&!e.hasOwnProperty(t)&&(a=!0),e[t]=r,a&&x(e,`toJSON`,n),(r!==i||t===`length`)&&x(e,t,n),!0},deleteProperty(e,t){if(typeof t!=`symbol`){x(e,t,n);let r=b(e,t);for(let e of n)(e[0]==r||e[0].startsWith(r+`/`))&&n.delete(e[0])}return delete e[t]}})}function b(e,t){let n=t,r=e?.[s];for(;r;)n=r[1]+`/`+n,r=r[0][s];return n}function x(e,t,i){let a=b(e,t),o=i.get(a);if(o)for(let e of o)n.delete(e),n.add(e),n.size==1&&queueMicrotask(()=>{let e={},t;for(let i of n)if(t=r.get(i),!(!t||t[u])&&!(t[s]&&n.has(t[s]))){if(e[i]=(e[i]||0)+1,e[i]>250){console.warn(`Possible infinite loop for watcher `+i+`:`,`
|
||||||
watch(`+t[p]?.toString()+(t[m]?`, `+t[m].toString():``)+`)`);continue}t.run()}n.clear()})}let S=new Proxy({},{get(e,t){return function(){return w(),O.call(void 0,t,...arguments)}}}),C=!1;function w(){if(C)return;C=!0;function e(t,n){for(let r of n)r[t]&&r[t](r),r.childNodes&&e(t,r.childNodes)}new MutationObserver(t=>{for(let n of t)e(`onmount`,n.addedNodes),e(`onunmount`,n.removedNodes)}).observe(document,{childList:!0,subtree:!0})}let T=Symbol(),E=Symbol(),D=Symbol();function O(e,t={},...n){let r=document.createElement(e);if(t)for(let e in t){let n=t[e],i=!1;e.length>5&&e.startsWith(`html-`)&&(i=!0,e=e.substring(5)),n===void 0?r.removeAttribute(e):typeof n!=`function`||e.length>2&&e.startsWith(`on`)?i?r.setAttribute(e,n):r[e]=n:(e==`rid`&&console.warn(`rid is provided as reactive function and will not have effect for `+r+`.
|
watch(`+t[p]?.toString()+(t[m]?`, `+t[m].toString():``)+`)`);continue}t.run()}n.clear()})}let S=new Proxy({},{get(e,t){return function(){return w(),O.call(void 0,t,...arguments)}}}),C=!1;function w(){if(C)return;C=!0;function e(t,n){for(let r of n)r[t]&&r[t](r),r.childNodes&&e(t,r.childNodes)}new MutationObserver(t=>{for(let n of t)e(`onmount`,n.addedNodes),e(`onunmount`,n.removedNodes)}).observe(document,{childList:!0,subtree:!0})}let T=Symbol(),E=Symbol(),D=Symbol();function O(e,t={},...n){let r=document.createElement(e);if(t)for(let e in t){let n=t[e],i=!1;e.length>5&&e.startsWith(`html-`)&&(i=!0,e=e.substring(5)),n===void 0?r.removeAttribute(e):typeof n!=`function`||e.length>2&&e.startsWith(`on`)?i?r.setAttribute(e,n):r[e]=n:(e==`rid`&&console.warn(`rid is provided as reactive function and will not have effect for `+r+`.
|
||||||
Consider using a plain number, string or object/array reference instead.`),r[T]=r[T]||[],r[T].push(()=>{if(!r)return;let t=n(r,e);t===void 0?(r[e]!==void 0&&(r[e]=void 0),r.removeAttribute(e)):i?r.setAttribute(e,t):r[e]=t}))}let i=r.onmount;r.onmount=()=>{if(!r[D]){if(r[D]=!0,r[T]){r[E]=r[E]||[];for(let e of r[T])r[E].push(g(e))}i?.(r)}};let a=r.onunmount;return r.onunmount=()=>{if(r[D]){if(r[D]=!1,r[E])for(let e of r[E])e.unwatch();r[E]=null,a?.(r)}},k(r,n),r}function k(e,t){t=N(t);for(let n of t)if(typeof n==`function`)A(e,n);else{let t=P(n);Array.isArray(t)?k(e,t):t&&e.appendChild(t)}}function A(e,t){let n=document.createComment(``);e.appendChild(n);let r=[],i=new Map,a=e.moveBefore||e.insertBefore;e[T]=e[T]||[],e[T].push(()=>{if(!e)return;let o=N(t(e)),s=o.length,c=new Map;if(!r?.length){let t=document.createDocumentFragment();for(let e=0;e<s;e++){o[e]=P(o[e],!0),t.appendChild(o[e]);let n=o[e].rid;n!==void 0&&(c.has(n)?console.warn(`Duplicated rid:`,n,o[e]):c.set(n,e))}e.insertBefore(t,n),t=null,r=o,i=c;return}let l=[],u=[],d=new Set,f=[];for(let e=0;e<s;e++){o[e]=P(o[e],!0);let t=o[e].rid;if(t!==void 0){c.has(t)?console.warn(`Duplicated rid:`,t,o[e]):c.set(t,e);let n=i.get(t);if(n>=0){d.add(r[n]),o[e]=r[n],f.push(n);continue}}u.push({child:o[e],prev:o[e-1]})}let p=j(f);f.length!=p.length&&f.forEach((e,t)=>{p.has(e)||l.push({child:r[e],targetPos:t})});let m,h,g;for(let t of l)m=r.findIndex(e=>e===t.child),h=t.targetPos,m<t.targetPos&&(h=t.targetPos+1),g=r[h]||n,M(r,m,t.targetPos),a.call(e,t.child,g);for(let e of u)e.prev?e.prev.after(e.child):(r[0]||n).before(e.child);for(let e=0;e<r.length;e++)d.has(r[e])||r[e].remove?.();r=o,i=c,o=null,c=null,d=null,l=null,u=null})}function j(e){let t=[],n=[];for(let r=0;r<e.length;r++){let i=e[r],a=0,o=0,s=t.length;for(;a<s;)o=Math.floor((a+s)/2),e[t[o]]>=i?s=o:a=o+1;a>0&&(n[r]=t[a-1]),t[a]=r}let r=new Set,i=t[t.length-1];for(;i!==void 0;)r.add(e[i]),i=n[i];return r}function M(e,t,n){if(t==n)return e;let r=t>n?-1:1,i=e[t];for(let i=t;i!=n;i+=r)e[i]=e[i+r];e[n]=i}function N(e){return e==null?[]:Array.isArray(e)?e:[e]}function P(e,t=!1){return t&&e==null&&(e=``),typeof e==`string`||typeof e==`number`||typeof e==`boolean`?document.createTextNode(e):e?.__raw===void 0?e:e.__raw}function F(e,t={fallbackPath:`#/`,transition:!0}){let n=L(e),r,i=()=>{let e=window.location.hash,i=I(n,e);if(!i){if(t.fallbackPath!=e){window.location.hash=t.fallbackPath;return}console.warn(`missing route:`,e);return}let a=async()=>{try{await r?.(),r=await i.handler(i)}catch(e){console.warn(`route navigation failed:`,e)}};t.transition&&document.startViewTransition?document.startViewTransition(a):a()};return window.addEventListener(`hashchange`,i),i(),()=>{window.removeEventListener(`hashchange`,i)}}function I(e,t){for(let n of e){let e=t.match(n.regex);if(!e)continue;let r={},i=t.split(`?`)?.[1];if(i){let e=new URLSearchParams(i);for(let[t,n]of e.entries())Array.isArray(r[t])||(r[t]=r[t]?[r[t]]:[]),r[t].push(n)}return Object.assign({path:t,query:r,params:e.groups||{}},n)}}function L(e){let t=[];for(let n in e){let r=n.split(`/`);for(let e in r)r[e].length>2&&r[e].startsWith(`{`)&&r[e].endsWith(`}`)?r[e]=`(?<`+r[e].substring(1,r[e].length-1)+`>[^\\/#?]+)`:r[e]=RegExp.escape(r[e]);t.push({regex:RegExp(`^`+r.join(`\\/`)+`(?:[?#].*)?$`),pattern:n,handler:e[n]})}return t}e.router=F,e.store=v,e.t=S,e.watch=g})(this.window=this.window||{});
|
Consider using a plain number, string or object/array reference instead.`),r[T]=r[T]||[],r[T].push(()=>{if(!r)return;let t=n(r,e);t===void 0?(r[e]!==void 0&&(r[e]=void 0),r.removeAttribute(e)):i?r.setAttribute(e,t):r[e]=t}))}let i=r.onmount;r.onmount=()=>{if(!r[D]){if(r[D]=!0,r[T]){r[E]=r[E]||[];for(let e of r[T])r[E].push(g(e))}i?.(r)}};let a=r.onunmount;return r.onunmount=()=>{if(r[D]){if(r[D]=!1,r[E])for(let e of r[E])e.unwatch();r[E]=null,a?.(r)}},k(r,n),r}function k(e,t){t=N(t);for(let n of t)if(typeof n==`function`)A(e,n);else{let t=P(n);Array.isArray(t)?k(e,t):t&&e.appendChild(t)}}function A(e,t){let n=document.createComment(``);e.appendChild(n);let r=[],i=new Map,a=e.moveBefore||e.insertBefore;e[T]=e[T]||[],e[T].push(()=>{if(!e)return;let o=N(t(e)),s=o.length,c=new Map;if(!r?.length){let t=document.createDocumentFragment();for(let e=0;e<s;e++){o[e]=P(o[e],!0),t.appendChild(o[e]);let n=o[e].rid;n!==void 0&&(c.has(n)?console.warn(`Duplicated rid:`,n,o[e]):c.set(n,e))}e.insertBefore(t,n),t=null,r=o,i=c;return}let l=[],u=[],d=new Set,f=[];for(let e=0;e<s;e++){o[e]=P(o[e],!0);let t=o[e].rid;if(t!==void 0){c.has(t)?console.warn(`Duplicated rid:`,t,o[e]):c.set(t,e);let n=i.get(t);if(n>=0){d.add(r[n]),o[e]=r[n],f.push(n);continue}}u.push({child:o[e],prev:o[e-1]})}let p=j(f);f.length!=p.length&&f.forEach((e,t)=>{p.has(e)||l.push({child:r[e],targetPos:t})});let m,h,g;for(let t of l)m=r.findIndex(e=>e===t.child),h=t.targetPos,m<t.targetPos&&(h=t.targetPos+1),g=r[h]||n,M(r,m,t.targetPos),a.call(e,t.child,g);for(let e of u)e.prev?e.prev.after(e.child):(r[0]||n).before(e.child);for(let e=0;e<r.length;e++)d.has(r[e])||r[e].remove?.();r=o,i=c,o=null,c=null,d=null,l=null,u=null})}function j(e){let t=[],n=[];for(let r=0;r<e.length;r++){let i=e[r],a=0,o=0,s=t.length;for(;a<s;)o=Math.floor((a+s)/2),e[t[o]]>=i?s=o:a=o+1;a>0&&(n[r]=t[a-1]),t[a]=r}let r=new Set,i=t[t.length-1];for(;i!==void 0;)r.add(e[i]),i=n[i];return r}function M(e,t,n){if(t==n)return e;let r=t>n?-1:1,i=e[t];for(let i=t;i!=n;i+=r)e[i]=e[i+r];e[n]=i}function N(e){return e==null?[]:Array.isArray(e)?e:[e]}function P(e,t=!1){return t&&e==null&&(e=``),typeof e==`string`||typeof e==`number`||typeof e==`boolean`?document.createTextNode(e):e?.__raw===void 0?e:e.__raw}function F(e,t={fallbackPath:`#/`,transition:!0}){let n=L(e),r,i=()=>{let e=window.location.hash,i=I(n,e);if(!i){if(t.fallbackPath!=e){window.location.hash=t.fallbackPath;return}console.warn(`missing route:`,e);return}let a=async()=>{try{await r?.(),r=await i.handler(i)}catch(e){console.warn(`route navigation failed:`,e)}};t.transition&&document.startViewTransition?document.startViewTransition(a):a()};return window.addEventListener(`hashchange`,i),i(),()=>{window.removeEventListener(`hashchange`,i)}}function I(e,t){for(let n of e){let e=t.match(n.regex);if(!e)continue;let r={},i=t.split(`?`)?.[1];if(i){let e=new URLSearchParams(i);for(let[t,n]of e.entries())Array.isArray(r[t])||(r[t]=r[t]?[r[t]]:[]),r[t].push(n)}return Object.assign({path:t,query:r,params:e.groups||{}},n)}}function L(e){let t=[];for(let n in e){let r=n.split(`/`);for(let e in r)r[e].length>2&&r[e].startsWith(`{`)&&r[e].endsWith(`}`)?r[e]=`(?<`+r[e].substring(1,r[e].length-1)+`>[^\\/#?]+)`:r[e]=RegExp.escape(r[e]);t.push({regex:RegExp(`^`+r.join(`\\/`)+`(?:[?#].*)?$`),pattern:n,handler:e[n]})}return t}e.router=F,e.store=v,e.t=S,e.watch=g})(this.window=this.window||{});
|
||||||
|
//# sourceMappingURL=shablon.iife.js.map
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
(function(e){let t,n=new Set,r=new Map,i=[],a,o=Symbol(),s=Symbol(),c=Symbol(),l=Symbol(),u=Symbol(),d=Symbol(),f=Symbol(),p=Symbol(),m=Symbol(),h=Symbol();function g(e,n){let d={[o]:`_`+Math.random(),[p]:e,[m]:n};return r.set(d[o],d),d.run=()=>{let e;t&&(e=t,d[s]=t[o],t[c]=t[c]||[],t[c].push(d[o])),d[l]?.forEach(e=>{e.delete(d[o])}),t=d;let n=d[p](d[h],d);d[m]&&(t=null,d[m](n,d[h],d)),d[h]=n,t=e},d.unwatch=function(){d[u]=1,i.push(d[o]),a&&clearTimeout(a),a=setTimeout(()=>{for(let e of i)_(e);i=[],a=null},50)},d.run(),d}function _(e){let t=r.get(e);if(t?.[d]?.(),t?.[c]){for(let e of t[c])_(e);t[s]=null,t[c]=null}if(t?.[l]){for(let n of t[l])n.delete(e);t[l]=null}r.delete(e)}function v(e){return y(e,new Map)}function y(e,n){let r=typeof e==`object`&&!Array.isArray(e)?Object.getOwnPropertyDescriptors(e):{};return new Proxy(e,{get(e,i,a){if(typeof i==`symbol`)return e[i];if(i==`__raw`)return e;if(r[i]?.get&&!e[s]){let n=i;if(i=`@@`+i,!t){let t=r[n].get.call(e);return r[n]._watcher&&(a[i]=t),t}let s=t[o];if(r[n]._refs=r[n]._refs||new Set,!r[n]._refs.has(s)){r[n]._refs.add(s);let e=t[d];t[d]=()=>{e?.(),r[n]._refs.delete(s),!r[n]._refs.size&&r[n]._watcher&&_(r[n]._watcher[o])}}if(!r[n]._watcher){let o=t;t=null;let s=g(r[n].get.bind(e),t=>{e.hasOwnProperty(i)?a[i]=t:Object.defineProperty(e,i,{writable:!0,enumerable:!1,value:t})});r[n]._watcher=s,s[d]=()=>{r[n]._watcher=null},t=o}}let c=e?.[i];if(typeof c==`function`)return c;if(typeof c==`object`&&c&&!c[s]&&(c.constructor?.name==`Object`||c.constructor?.name==`Array`||c.constructor?.name==null)&&(c[s]=[a,i],c=y(c,n),e[i]=c),t){let r=b(e,i),a=t[o],s=[r];t[l]=t[l]||new Set;for(let e of s){let r=n.get(e);r||(r=new Set,n.set(e,r)),r.add(a),t[l].add(r)}}return c},set(e,t,r){if(typeof t==`symbol`)return e[t]=r,!0;let i=e[t];i?.[s]&&(i[f]=!0),r?.[s]&&Array.isArray(e)&&!isNaN(t)&&(r[s][1]=t);let a=!1;return i===void 0&&!e.hasOwnProperty(t)&&(a=!0),e[t]=r,a&&x(e,`toJSON`,n),(r!==i||t===`length`)&&x(e,t,n),!0},deleteProperty(e,t){if(typeof t!=`symbol`){x(e,t,n);let r=b(e,t);for(let e of n)(e[0]==r||e[0].startsWith(r+`/`))&&n.delete(e[0])}return delete e[t]}})}function b(e,t){let n=t,r=e?.[s];for(;r;)n=r[1]+`/`+n,r=r[0][s];return n}function x(e,t,i){let a=b(e,t),o=i.get(a);if(o)for(let e of o)n.delete(e),n.add(e),n.size==1&&queueMicrotask(()=>{let e={},t;for(let i of n)if(t=r.get(i),!(!t||t[u])&&!(t[s]&&n.has(t[s]))){if(e[i]=(e[i]||0)+1,e[i]>250){console.warn(`Possible infinite loop for watcher `+i+`:`,`
|
(function(e){let t,n=new Set,r=new Map,i=[],a,o=Symbol(),s=Symbol(),c=Symbol(),l=Symbol(),u=Symbol(),d=Symbol(),f=Symbol(),p=Symbol(),m=Symbol(),h=Symbol();function g(e,n){let d={[o]:`_`+Math.random(),[p]:e,[m]:n};return r.set(d[o],d),d.run=()=>{let e;t&&(e=t,d[s]=t[o],t[c]=t[c]||[],t[c].push(d[o])),d[l]?.forEach(e=>{e.delete(d[o])}),t=d;let n=d[p](d[h],d);d[m]&&(t=null,d[m](n,d[h],d)),d[h]=n,t=e},d.unwatch=function(){d[u]=1,i.push(d[o]),a&&clearTimeout(a),a=setTimeout(()=>{for(let e of i)_(e);i=[],a=null},50)},d.run(),d}function _(e){let t=r.get(e);if(t?.[d]?.(),t?.[c]){for(let e of t[c])_(e);t[s]=null,t[c]=null}if(t?.[l]){for(let n of t[l])n.delete(e);t[l]=null}r.delete(e)}function v(e){return y(e,new Map)}function y(e,n){let r=typeof e==`object`&&!Array.isArray(e)?Object.getOwnPropertyDescriptors(e):{};return new Proxy(e,{get(e,i,a){if(typeof i==`symbol`)return e[i];if(i==`__raw`)return e;if(r[i]?.get&&!e[s]){let n=i;if(i=`@@`+i,!t){let t=r[n].get.call(e);return r[n]._watcher&&(a[i]=t),t}let s=t[o];if(r[n]._refs=r[n]._refs||new Set,!r[n]._refs.has(s)){r[n]._refs.add(s);let e=t[d];t[d]=()=>{e?.(),r[n]._refs.delete(s),!r[n]._refs.size&&r[n]._watcher&&_(r[n]._watcher[o])}}if(!r[n]._watcher){let o=t;t=null;let s=g(r[n].get.bind(e),t=>{e.hasOwnProperty(i)?a[i]=t:Object.defineProperty(e,i,{writable:!0,enumerable:!1,value:t})});r[n]._watcher=s,s[d]=()=>{r[n]._watcher=null},t=o}}let c=e?.[i];if(typeof c==`function`)return c;if(typeof c==`object`&&c&&!c[s]&&(c.constructor?.name==`Object`||c.constructor?.name==`Array`||c.constructor?.name==null)&&(c[s]=[a,i],c=y(c,n),e[i]=c),t){let r=b(e,i),a=t[o],s=[r];t[l]=t[l]||new Set;for(let e of s){let r=n.get(e);r||(r=new Set,n.set(e,r)),r.add(a),t[l].add(r)}}return c},set(e,t,r){if(typeof t==`symbol`)return e[t]=r,!0;let i=e[t];i?.[s]&&(i[f]=!0),r?.[s]&&Array.isArray(e)&&!isNaN(t)&&(r[s][1]=t);let a=!1;return i===void 0&&!e.hasOwnProperty(t)&&(a=!0),e[t]=r,a&&x(e,`toJSON`,n),(r!==i||t===`length`)&&x(e,t,n),!0},deleteProperty(e,t){if(typeof t!=`symbol`){x(e,t,n);let r=b(e,t);for(let e of n)(e[0]==r||e[0].startsWith(r+`/`))&&n.delete(e[0])}return delete e[t]}})}function b(e,t){let n=t,r=e?.[s];for(;r;)n=r[1]+`/`+n,r=r[0][s];return n}function x(e,t,i){let a=b(e,t),o=i.get(a);if(o)for(let e of o)n.delete(e),n.add(e),n.size==1&&queueMicrotask(()=>{let e={},t;for(let i of n)if(t=r.get(i),!(!t||t[u])&&!(t[s]&&n.has(t[s]))){if(e[i]=(e[i]||0)+1,e[i]>250){console.warn(`Possible infinite loop for watcher `+i+`:`,`
|
||||||
watch(`+t[p]?.toString()+(t[m]?`, `+t[m].toString():``)+`)`);continue}t.run()}n.clear()})}let S=new Proxy({},{get(e,t){return function(){return w(),O.call(void 0,t,...arguments)}}}),C=!1;function w(){if(C)return;C=!0;function e(t,n){for(let r of n)r[t]&&r[t](r),r.childNodes&&e(t,r.childNodes)}new MutationObserver(t=>{for(let n of t)e(`onmount`,n.addedNodes),e(`onunmount`,n.removedNodes)}).observe(document,{childList:!0,subtree:!0})}let T=Symbol(),E=Symbol(),D=Symbol();function O(e,t={},...n){let r=document.createElement(e);if(t)for(let e in t){let n=t[e],i=!1;e.length>5&&e.startsWith(`html-`)&&(i=!0,e=e.substring(5)),n===void 0?r.removeAttribute(e):typeof n!=`function`||e.length>2&&e.startsWith(`on`)?i?r.setAttribute(e,n):r[e]=n:(e==`rid`&&console.warn(`rid is provided as reactive function and will not have effect for `+r+`.
|
watch(`+t[p]?.toString()+(t[m]?`, `+t[m].toString():``)+`)`);continue}t.run()}n.clear()})}let S=new Proxy({},{get(e,t){return function(){return w(),O.call(void 0,t,...arguments)}}}),C=!1;function w(){if(C)return;C=!0;function e(t,n){for(let r of n)r[t]&&r[t](r),r.childNodes&&e(t,r.childNodes)}new MutationObserver(t=>{for(let n of t)e(`onmount`,n.addedNodes),e(`onunmount`,n.removedNodes)}).observe(document,{childList:!0,subtree:!0})}let T=Symbol(),E=Symbol(),D=Symbol();function O(e,t={},...n){let r=document.createElement(e);if(t)for(let e in t){let n=t[e],i=!1;e.length>5&&e.startsWith(`html-`)&&(i=!0,e=e.substring(5)),n===void 0?r.removeAttribute(e):typeof n!=`function`||e.length>2&&e.startsWith(`on`)?i?r.setAttribute(e,n):r[e]=n:(e==`rid`&&console.warn(`rid is provided as reactive function and will not have effect for `+r+`.
|
||||||
Consider using a plain number, string or object/array reference instead.`),r[T]=r[T]||[],r[T].push(()=>{if(!r)return;let t=n(r,e);t===void 0?(r[e]!==void 0&&(r[e]=void 0),r.removeAttribute(e)):i?r.setAttribute(e,t):r[e]=t}))}let i=r.onmount;r.onmount=()=>{if(!r[D]){if(r[D]=!0,r[T]){r[E]=r[E]||[];for(let e of r[T])r[E].push(g(e))}i?.(r)}};let a=r.onunmount;return r.onunmount=()=>{if(r[D]){if(r[D]=!1,r[E])for(let e of r[E])e.unwatch();r[E]=null,a?.(r)}},k(r,n),r}function k(e,t){t=N(t);for(let n of t)if(typeof n==`function`)A(e,n);else{let t=P(n);Array.isArray(t)?k(e,t):t&&e.appendChild(t)}}function A(e,t){let n=document.createComment(``);e.appendChild(n);let r=[],i=new Map,a=e.moveBefore||e.insertBefore;e[T]=e[T]||[],e[T].push(()=>{if(!e)return;let o=N(t(e)),s=o.length,c=new Map;if(!r?.length){let t=document.createDocumentFragment();for(let e=0;e<s;e++){o[e]=P(o[e],!0),t.appendChild(o[e]);let n=o[e].rid;n!==void 0&&(c.has(n)?console.warn(`Duplicated rid:`,n,o[e]):c.set(n,e))}e.insertBefore(t,n),t=null,r=o,i=c;return}let l=[],u=[],d=new Set,f=[];for(let e=0;e<s;e++){o[e]=P(o[e],!0);let t=o[e].rid;if(t!==void 0){c.has(t)?console.warn(`Duplicated rid:`,t,o[e]):c.set(t,e);let n=i.get(t);if(n>=0){d.add(r[n]),o[e]=r[n],f.push(n);continue}}u.push({child:o[e],prev:o[e-1]})}let p=j(f);f.length!=p.length&&f.forEach((e,t)=>{p.has(e)||l.push({child:r[e],targetPos:t})});let m,h,g;for(let t of l)m=r.findIndex(e=>e===t.child),h=t.targetPos,m<t.targetPos&&(h=t.targetPos+1),g=r[h]||n,M(r,m,t.targetPos),a.call(e,t.child,g);for(let e of u)e.prev?e.prev.after(e.child):(r[0]||n).before(e.child);for(let e=0;e<r.length;e++)d.has(r[e])||r[e].remove?.();r=o,i=c,o=null,c=null,d=null,l=null,u=null})}function j(e){let t=[],n=[];for(let r=0;r<e.length;r++){let i=e[r],a=0,o=0,s=t.length;for(;a<s;)o=Math.floor((a+s)/2),e[t[o]]>=i?s=o:a=o+1;a>0&&(n[r]=t[a-1]),t[a]=r}let r=new Set,i=t[t.length-1];for(;i!==void 0;)r.add(e[i]),i=n[i];return r}function M(e,t,n){if(t==n)return e;let r=t>n?-1:1,i=e[t];for(let i=t;i!=n;i+=r)e[i]=e[i+r];e[n]=i}function N(e){return e==null?[]:Array.isArray(e)?e:[e]}function P(e,t=!1){return t&&e==null&&(e=``),typeof e==`string`||typeof e==`number`||typeof e==`boolean`?document.createTextNode(e):e?.__raw===void 0?e:e.__raw}function F(e,t={fallbackPath:`#/`,transition:!0}){let n=L(e),r,i=()=>{let e=window.location.hash,i=I(n,e);if(!i){if(t.fallbackPath!=e){window.location.hash=t.fallbackPath;return}console.warn(`missing route:`,e);return}let a=async()=>{try{await r?.(),r=await i.handler(i)}catch(e){console.warn(`route navigation failed:`,e)}};t.transition&&document.startViewTransition?document.startViewTransition(a):a()};return window.addEventListener(`hashchange`,i),i(),()=>{window.removeEventListener(`hashchange`,i)}}function I(e,t){for(let n of e){let e=t.match(n.regex);if(!e)continue;let r={},i=t.split(`?`)?.[1];if(i){let e=new URLSearchParams(i);for(let[t,n]of e.entries())Array.isArray(r[t])||(r[t]=r[t]?[r[t]]:[]),r[t].push(n)}return Object.assign({path:t,query:r,params:e.groups||{}},n)}}function L(e){let t=[];for(let n in e){let r=n.split(`/`);for(let e in r)r[e].length>2&&r[e].startsWith(`{`)&&r[e].endsWith(`}`)?r[e]=`(?<`+r[e].substring(1,r[e].length-1)+`>[^\\/#?]+)`:r[e]=RegExp.escape(r[e]);t.push({regex:RegExp(`^`+r.join(`\\/`)+`(?:[?#].*)?$`),pattern:n,handler:e[n]})}return t}e.router=F,e.store=v,e.t=S,e.watch=g})(this.window=this.window||{});
|
Consider using a plain number, string or object/array reference instead.`),r[T]=r[T]||[],r[T].push(()=>{if(!r)return;let t=n(r,e);t===void 0?(r[e]!==void 0&&(r[e]=void 0),r.removeAttribute(e)):i?r.setAttribute(e,t):r[e]=t}))}let i=r.onmount;r.onmount=()=>{if(!r[D]){if(r[D]=!0,r[T]){r[E]=r[E]||[];for(let e of r[T])r[E].push(g(e))}i?.(r)}};let a=r.onunmount;return r.onunmount=()=>{if(r[D]){if(r[D]=!1,r[E])for(let e of r[E])e.unwatch();r[E]=null,a?.(r)}},k(r,n),r}function k(e,t){t=N(t);for(let n of t)if(typeof n==`function`)A(e,n);else{let t=P(n);Array.isArray(t)?k(e,t):t&&e.appendChild(t)}}function A(e,t){let n=document.createComment(``);e.appendChild(n);let r=[],i=new Map,a=e.moveBefore||e.insertBefore;e[T]=e[T]||[],e[T].push(()=>{if(!e)return;let o=N(t(e)),s=o.length,c=new Map;if(!r?.length){let t=document.createDocumentFragment();for(let e=0;e<s;e++){o[e]=P(o[e],!0),t.appendChild(o[e]);let n=o[e].rid;n!==void 0&&(c.has(n)?console.warn(`Duplicated rid:`,n,o[e]):c.set(n,e))}e.insertBefore(t,n),t=null,r=o,i=c;return}let l=[],u=[],d=new Set,f=[];for(let e=0;e<s;e++){o[e]=P(o[e],!0);let t=o[e].rid;if(t!==void 0){c.has(t)?console.warn(`Duplicated rid:`,t,o[e]):c.set(t,e);let n=i.get(t);if(n>=0){d.add(r[n]),o[e]=r[n],f.push(n);continue}}u.push({child:o[e],prev:o[e-1]})}let p=j(f);f.length!=p.length&&f.forEach((e,t)=>{p.has(e)||l.push({child:r[e],targetPos:t})});let m,h,g;for(let t of l)m=r.findIndex(e=>e===t.child),h=t.targetPos,m<t.targetPos&&(h=t.targetPos+1),g=r[h]||n,M(r,m,t.targetPos),a.call(e,t.child,g);for(let e of u)e.prev?e.prev.after(e.child):(r[0]||n).before(e.child);for(let e=0;e<r.length;e++)d.has(r[e])||r[e].remove?.();r=o,i=c,o=null,c=null,d=null,l=null,u=null})}function j(e){let t=[],n=[];for(let r=0;r<e.length;r++){let i=e[r],a=0,o=0,s=t.length;for(;a<s;)o=Math.floor((a+s)/2),e[t[o]]>=i?s=o:a=o+1;a>0&&(n[r]=t[a-1]),t[a]=r}let r=new Set,i=t[t.length-1];for(;i!==void 0;)r.add(e[i]),i=n[i];return r}function M(e,t,n){if(t==n)return e;let r=t>n?-1:1,i=e[t];for(let i=t;i!=n;i+=r)e[i]=e[i+r];e[n]=i}function N(e){return e==null?[]:Array.isArray(e)?e:[e]}function P(e,t=!1){return t&&e==null&&(e=``),typeof e==`string`||typeof e==`number`||typeof e==`boolean`?document.createTextNode(e):e?.__raw===void 0?e:e.__raw}function F(e,t={fallbackPath:`#/`,transition:!0}){let n=L(e),r,i=()=>{let e=window.location.hash,i=I(n,e);if(!i){if(t.fallbackPath!=e){window.location.hash=t.fallbackPath;return}console.warn(`missing route:`,e);return}let a=async()=>{try{await r?.(),r=await i.handler(i)}catch(e){console.warn(`route navigation failed:`,e)}};t.transition&&document.startViewTransition?document.startViewTransition(a):a()};return window.addEventListener(`hashchange`,i),i(),()=>{window.removeEventListener(`hashchange`,i)}}function I(e,t){for(let n of e){let e=t.match(n.regex);if(!e)continue;let r={},i=t.split(`?`)?.[1];if(i){let e=new URLSearchParams(i);for(let[t,n]of e.entries())Array.isArray(r[t])||(r[t]=r[t]?[r[t]]:[]),r[t].push(n)}return Object.assign({path:t,query:r,params:e.groups||{}},n)}}function L(e){let t=[];for(let n in e){let r=n.split(`/`);for(let e in r)r[e].length>2&&r[e].startsWith(`{`)&&r[e].endsWith(`}`)?r[e]=`(?<`+r[e].substring(1,r[e].length-1)+`>[^\\/#?]+)`:r[e]=RegExp.escape(r[e]);t.push({regex:RegExp(`^`+r.join(`\\/`)+`(?:[?#].*)?$`),pattern:n,handler:e[n]})}return t}e.router=F,e.store=v,e.t=S,e.watch=g})(this.window=this.window||{});
|
||||||
|
//# sourceMappingURL=shablon.iife.js.map
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ export function pageCollections(route) {
|
|||||||
t.div(
|
t.div(
|
||||||
{ className: "page-content full-height" },
|
{ className: "page-content full-height" },
|
||||||
t.header(
|
t.header(
|
||||||
{ className: "page-header compact flex-nowrap" },
|
{ className: "page-header flex-nowrap" },
|
||||||
t.nav(
|
t.nav(
|
||||||
{ className: "breadcrumbs" },
|
{ className: "breadcrumbs" },
|
||||||
t.div(null, "Collections"),
|
t.div(null, "Collections"),
|
||||||
|
|||||||
@@ -35,3 +35,4 @@
|
|||||||
@import "./collectionsOverview.css";
|
@import "./collectionsOverview.css";
|
||||||
@import "./erd.css";
|
@import "./erd.css";
|
||||||
@import "./hideControls.css";
|
@import "./hideControls.css";
|
||||||
|
@import "./sqlConsole.css";
|
||||||
|
|||||||
@@ -919,6 +919,7 @@ hr {
|
|||||||
.alert {
|
.alert {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
min-height: var(--lgBtnHeight);
|
min-height: var(--lgBtnHeight);
|
||||||
padding: max(5px, calc(var(--inputPadding) - 5px)) max(10px, var(--inputPadding));
|
padding: max(5px, calc(var(--inputPadding) - 5px)) max(10px, var(--inputPadding));
|
||||||
|
|||||||
@@ -1131,6 +1131,7 @@ button {
|
|||||||
&:empty::before {
|
&:empty::before {
|
||||||
content: attr(data-placeholder);
|
content: attr(data-placeholder);
|
||||||
color: var(--surfaceTxtDisabledColor);
|
color: var(--surfaceTxtDisabledColor);
|
||||||
|
white-space: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-10
@@ -174,16 +174,14 @@
|
|||||||
column-gap: var(--smSpacing);
|
column-gap: var(--smSpacing);
|
||||||
row-gap: calc(var(--smSpacing) - 5px);
|
row-gap: calc(var(--smSpacing) - 5px);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: -5px 0 calc(var(--spacing) - 5px);
|
min-height: var(--btnHeight);
|
||||||
|
margin: -10px 0 calc(var(--spacing) - 10px);
|
||||||
.searchbar {
|
.searchbar {
|
||||||
width: auto;
|
width: auto;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
&.compact {
|
|
||||||
margin-top: -10px;
|
|
||||||
margin-bottom: calc(var(--spacing) - 10px);
|
|
||||||
}
|
|
||||||
.page-header-secondary-btns,
|
.page-header-secondary-btns,
|
||||||
.page-header-primary-btns {
|
.page-header-primary-btns {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -206,10 +204,6 @@
|
|||||||
}
|
}
|
||||||
@media (max-width: 950px) {
|
@media (max-width: 950px) {
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
flex-grow: 1;
|
|
||||||
.flex-nowrap & {
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
|
||||||
.btn {
|
.btn {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-basis: 0;
|
flex-basis: 0;
|
||||||
@@ -219,7 +213,6 @@
|
|||||||
}
|
}
|
||||||
@media (max-width: 550px) {
|
@media (max-width: 550px) {
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
flex-grow: 0;
|
|
||||||
.api-preview-btn {
|
.api-preview-btn {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -228,6 +221,7 @@
|
|||||||
width: auto;
|
width: auto;
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
min-width: 0;
|
||||||
i + .txt {
|
i + .txt {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -241,6 +235,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
width: auto;
|
width: auto;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
gap: var(--smSpacing);
|
||||||
color: var(--surfaceTxtHintColor);
|
color: var(--surfaceTxtHintColor);
|
||||||
font-size: var(--smFontSize);
|
font-size: var(--smFontSize);
|
||||||
line-height: var(--smLineHeight);
|
line-height: var(--smLineHeight);
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
.field.sql-console-field {
|
||||||
|
.input {
|
||||||
|
resize: vertical;
|
||||||
|
height: 250px;
|
||||||
|
min-height: 150px;
|
||||||
|
max-height: 400px;
|
||||||
|
|
||||||
|
@media (max-height: 650px) {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sql-console-history-dropdown {
|
||||||
|
width: 450px;
|
||||||
|
&.no-items {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
.dropdown-item {
|
||||||
|
gap: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-family: var(--monospaceFontFamily);
|
||||||
|
.query {
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -317,6 +317,22 @@ table {
|
|||||||
min-width: 900px;
|
min-width: 900px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
table > :first-child > tr:first-child {
|
||||||
|
> :first-child {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
}
|
||||||
|
> :last-child {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table > :last-child > tr:last-child {
|
||||||
|
> :first-child {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
> :last-child {
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
tr {
|
tr {
|
||||||
animation: fadeIn var(--animationSpeed);
|
animation: fadeIn var(--animationSpeed);
|
||||||
}
|
}
|
||||||
@@ -342,6 +358,7 @@ table {
|
|||||||
td,
|
td,
|
||||||
th {
|
th {
|
||||||
height: 59px;
|
height: 59px;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
line-height: var(--smLineHeight);
|
line-height: var(--smLineHeight);
|
||||||
font-family: var(--baseFontFamily);
|
font-family: var(--baseFontFamily);
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
margin: 3px;
|
|
||||||
padding: 3px 5px;
|
padding: 3px 5px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ export function logsList(logsSettings) {
|
|||||||
return; // nothing to download
|
return; // nothing to download
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(data.bulkSelected);
|
||||||
|
|
||||||
if (selected.length == 1) {
|
if (selected.length == 1) {
|
||||||
return app.utils.downloadJSON(
|
return app.utils.downloadJSON(
|
||||||
selected[0],
|
selected[0],
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { pageApplicationSettings } from "@/settings/application/pageApplicationS
|
|||||||
import { pageBackupsSettings } from "@/settings/backups/pageBackupsSettings";
|
import { pageBackupsSettings } from "@/settings/backups/pageBackupsSettings";
|
||||||
import { pageCronsSettings } from "@/settings/crons/pageCronsSettings";
|
import { pageCronsSettings } from "@/settings/crons/pageCronsSettings";
|
||||||
import { pageMailSettings } from "@/settings/mail/pageMailSettings";
|
import { pageMailSettings } from "@/settings/mail/pageMailSettings";
|
||||||
|
import { pageSQLConsole } from "@/settings/sql/pageSQLConsole";
|
||||||
import { pageStorageSettings } from "@/settings/storage/pageStorageSettings";
|
import { pageStorageSettings } from "@/settings/storage/pageStorageSettings";
|
||||||
import { pageExportCollections } from "@/settings/sync/pageExportCollections";
|
import { pageExportCollections } from "@/settings/sync/pageExportCollections";
|
||||||
import { pageImportCollections } from "@/settings/sync/pageImportCollections";
|
import { pageImportCollections } from "@/settings/sync/pageImportCollections";
|
||||||
@@ -170,3 +171,4 @@ app.routes.superuserOnly("#/settings/backups", pageBackupsSettings);
|
|||||||
app.routes.superuserOnly("#/settings/crons", pageCronsSettings);
|
app.routes.superuserOnly("#/settings/crons", pageCronsSettings);
|
||||||
app.routes.superuserOnly("#/settings/export-collections", pageExportCollections);
|
app.routes.superuserOnly("#/settings/export-collections", pageExportCollections);
|
||||||
app.routes.superuserOnly("#/settings/import-collections", pageImportCollections);
|
app.routes.superuserOnly("#/settings/import-collections", pageImportCollections);
|
||||||
|
app.routes.superuserOnly("#/settings/sql", pageSQLConsole);
|
||||||
|
|||||||
@@ -0,0 +1,486 @@
|
|||||||
|
import { settingsSidebar } from "../settingsSidebar";
|
||||||
|
|
||||||
|
const SQL_HISTORY_STORAGE_KEY = "pbSQLConsoleHistory";
|
||||||
|
|
||||||
|
export function pageSQLConsole(route) {
|
||||||
|
app.store.title = "SQL console";
|
||||||
|
|
||||||
|
const uniqueId = "sql_console_" + app.utils.randomString();
|
||||||
|
const editorId = uniqueId + "editor";
|
||||||
|
const requestKey = uniqueId + "executeSQL";
|
||||||
|
const defaultMaxRows = 250;
|
||||||
|
|
||||||
|
const pageData = store({
|
||||||
|
askedForConfirmationAtLeastOnce: false,
|
||||||
|
isExecuting: false,
|
||||||
|
maxRows: defaultMaxRows,
|
||||||
|
query: "",
|
||||||
|
result: {},
|
||||||
|
sort: {},
|
||||||
|
errorMsg: "",
|
||||||
|
executedHistory: app.utils.getLocalHistory(SQL_HISTORY_STORAGE_KEY, []),
|
||||||
|
|
||||||
|
get sortedResultRows() {
|
||||||
|
// no client-side sort
|
||||||
|
if (typeof pageData.sort?.index == "undefined") {
|
||||||
|
const rows = pageData.result?.rows || [];
|
||||||
|
return pageData.maxRows >= rows.length ? rows : rows.slice(0, pageData.maxRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAsc = !!pageData.sort?.asc;
|
||||||
|
|
||||||
|
const sorted = pageData.result?.rows?.toSorted((rowA, rowB) => {
|
||||||
|
const valA = rowA[pageData.sort.index];
|
||||||
|
const valB = rowB[pageData.sort.index];
|
||||||
|
|
||||||
|
if (isAsc) {
|
||||||
|
if (valA == valB) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (valA == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (valB == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return valA.localeCompare(valB);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valA == valB) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (valA == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (valB == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return valB.localeCompare(valA);
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
return pageData.maxRows >= sorted.length ? sorted : sorted.slice(0, pageData.maxRows);
|
||||||
|
},
|
||||||
|
get totalRemainingRows() {
|
||||||
|
return (pageData.result?.rows?.length << 0) - pageData.sortedResultRows.length;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function needConfirmation(query) {
|
||||||
|
if (!query?.trim()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the hideControls option is enabled ask for confirmation
|
||||||
|
// at least once no matter of the query
|
||||||
|
if (
|
||||||
|
!pageData.askedForConfirmationAtLeastOnce
|
||||||
|
&& !!app.store.settings?.meta?.hideControls
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query?.replace(/[\s\;]/gm, " ").toUpperCase() + " ";
|
||||||
|
|
||||||
|
return !![
|
||||||
|
"INSERT ",
|
||||||
|
"CREATE ",
|
||||||
|
"UPDATE ",
|
||||||
|
"DELETE ",
|
||||||
|
"DROP ",
|
||||||
|
"DETACH ",
|
||||||
|
"PRAGMA ",
|
||||||
|
].find((p) => query.includes(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeSQLWithConfirm() {
|
||||||
|
if (!needConfirmation(pageData.query)) {
|
||||||
|
return executeSQL();
|
||||||
|
}
|
||||||
|
|
||||||
|
pageData.askedForConfirmationAtLeastOnce = true;
|
||||||
|
|
||||||
|
return app.modals.confirm(
|
||||||
|
t.div(
|
||||||
|
{ className: "txt-center" },
|
||||||
|
t.h6(
|
||||||
|
null,
|
||||||
|
"Be careful and continue only if you really know what you are doing because, depending on the query, the operation could break your application and may not be reversible.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
() => executeSQL(),
|
||||||
|
null,
|
||||||
|
{ yesButton: "Execute", noButton: "Cancel" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeSQL() {
|
||||||
|
pageData.isExecuting = true;
|
||||||
|
pageData.maxRows = defaultMaxRows;
|
||||||
|
pageData.result = {};
|
||||||
|
pageData.sort = {};
|
||||||
|
pageData.errorMsg = "";
|
||||||
|
|
||||||
|
const query = pageData.query.trim();
|
||||||
|
if (!query) {
|
||||||
|
app.pb.cancelRequest(requestKey);
|
||||||
|
pageData.isExecuting = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// @todo add method to JS SDK
|
||||||
|
pageData.result = await app.pb.send("/api/sql", {
|
||||||
|
method: "POST",
|
||||||
|
body: { query },
|
||||||
|
requestKey: requestKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
addToHistory(query);
|
||||||
|
|
||||||
|
pageData.isExecuting = false;
|
||||||
|
} catch (err) {
|
||||||
|
if (!err?.isAbort) {
|
||||||
|
pageData.isExecuting = false;
|
||||||
|
pageData.errorMsg = err?.response?.message || err?.message || "Failed to execute query.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFromHistory(query) {
|
||||||
|
function looseNormalize(str) {
|
||||||
|
return str.replace(/[\s\;]/gm, "").toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = looseNormalize(query);
|
||||||
|
|
||||||
|
for (let i = pageData.executedHistory.length - 1; i >= 0; i--) {
|
||||||
|
if (
|
||||||
|
pageData.executedHistory[i] == query
|
||||||
|
|| looseNormalize(pageData.executedHistory[i]) == normalized
|
||||||
|
) {
|
||||||
|
pageData.executedHistory.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addToHistory(query) {
|
||||||
|
removeFromHistory(query);
|
||||||
|
|
||||||
|
pageData.executedHistory.unshift(pageData.query);
|
||||||
|
if (pageData.executedHistory.length > 10) {
|
||||||
|
pageData.executedHistory.splice(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadCSV() {
|
||||||
|
if (!pageData.sortedResultRows.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = [pageData.result.columns.map((c) => c.name)].concat(pageData.sortedResultRows);
|
||||||
|
|
||||||
|
const name = "export_" + app.utils.toLocalDatetime(new Date()).replace(/[\-\:\. ]/g, "_") + ".csv";
|
||||||
|
|
||||||
|
app.utils.downloadCSV(data, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSort(index) {
|
||||||
|
if (pageData.sort?.index == index) {
|
||||||
|
pageData.sort = {
|
||||||
|
index: index,
|
||||||
|
asc: !pageData.sort.asc,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
pageData.sort = {
|
||||||
|
index: index,
|
||||||
|
asc: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const watchers = [
|
||||||
|
watch(() => JSON.stringify(pageData.executedHistory), (newVal, oldVal) => {
|
||||||
|
if (typeof oldVal == "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.localStorage.setItem(SQL_HISTORY_STORAGE_KEY, newVal);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
return t.div(
|
||||||
|
{
|
||||||
|
pbEvent: "pageSQLConsole",
|
||||||
|
className: "page",
|
||||||
|
onunmount: () => {
|
||||||
|
app.pb.cancelRequest(requestKey);
|
||||||
|
watchers.forEach((w) => w?.unwatch());
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settingsSidebar(),
|
||||||
|
t.div(
|
||||||
|
{ className: "page-content full-height" },
|
||||||
|
t.header(
|
||||||
|
{ className: "page-header" },
|
||||||
|
t.nav(
|
||||||
|
{ className: "breadcrumbs" },
|
||||||
|
t.div({ className: "breadcrumb-item" }, "Settings"),
|
||||||
|
t.div({ className: "breadcrumb-item" }, () => app.store.title),
|
||||||
|
),
|
||||||
|
t.div(
|
||||||
|
{ className: "page-header-secondary-btns" },
|
||||||
|
t.button(
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
className: "btn circle transparent secondary",
|
||||||
|
ariaDescription: app.attrs.tooltip("Recently executed queries", "right"),
|
||||||
|
"html-popovertarget": "sql-console-history-dropdown",
|
||||||
|
},
|
||||||
|
t.i({ className: "ri-history-line", ariaHidden: true }),
|
||||||
|
),
|
||||||
|
t.div(
|
||||||
|
{
|
||||||
|
id: "sql-console-history-dropdown",
|
||||||
|
className: () =>
|
||||||
|
`dropdown left sql-console-history-dropdown ${
|
||||||
|
!pageData.executedHistory.length ? "no-items" : ""
|
||||||
|
}`,
|
||||||
|
popover: "auto",
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (!pageData.executedHistory.length) {
|
||||||
|
return t.span({ className: "txt txt-hint p-5" }, "No recently executed queries.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageData.executedHistory.map((item) => {
|
||||||
|
return t.button(
|
||||||
|
{
|
||||||
|
role: "button",
|
||||||
|
className: "dropdown-item",
|
||||||
|
onclick: (e) => {
|
||||||
|
e.target.closest(".dropdown").hidePopover();
|
||||||
|
pageData.query = item;
|
||||||
|
|
||||||
|
document.getElementById(editorId)?.click();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
t.span(
|
||||||
|
{ className: "query" },
|
||||||
|
() => app.utils.truncate(item, 500),
|
||||||
|
),
|
||||||
|
t.small(
|
||||||
|
{
|
||||||
|
role: "button",
|
||||||
|
className: "remove-btn link-hint m-l-auto p-l-5 p-r-5",
|
||||||
|
title: "Clear",
|
||||||
|
onauxclick: (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
onclick: (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
removeFromHistory(item);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
t.i({ className: "ri-close-line", ariaHidden: true }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
t.div(
|
||||||
|
{ className: "page-header-primary-btns" },
|
||||||
|
t.button(
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
className: () => `btn expanded-lg ${pageData.isExecuting ? "loading" : ""}`,
|
||||||
|
disabled: () => pageData.isExecuting,
|
||||||
|
onclick: () => executeSQLWithConfirm(),
|
||||||
|
},
|
||||||
|
t.i({ className: "ri-play-large-line", ariaHidden: true }),
|
||||||
|
t.span({ className: "txt" }, "Execute"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
t.div(
|
||||||
|
{ className: "field sql-console-field" },
|
||||||
|
app.components.codeEditor({
|
||||||
|
id: editorId,
|
||||||
|
language: "sql",
|
||||||
|
required: true,
|
||||||
|
name: "query",
|
||||||
|
placeholder: "e.g. EXPLAIN QUERY PLAN SELECT * from users WHERE verified=true",
|
||||||
|
value: () => pageData.query,
|
||||||
|
oninput: (val) => pageData.query = val,
|
||||||
|
onblur: (val) => pageData.query = val.trim(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
t.div(
|
||||||
|
{ className: "flex field-help m-b-sm" },
|
||||||
|
t.button(
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
className: "link-hint m-l-auto",
|
||||||
|
"html-popovertarget": uniqueId + "caveats_dropdown",
|
||||||
|
},
|
||||||
|
() => "SQL console caveats",
|
||||||
|
),
|
||||||
|
t.div(
|
||||||
|
{
|
||||||
|
id: uniqueId + "caveats_dropdown",
|
||||||
|
className: "dropdown sm query-caveats-dropdown",
|
||||||
|
popover: "auto",
|
||||||
|
},
|
||||||
|
t.ul(
|
||||||
|
null,
|
||||||
|
t.li(null, "The returned rows are limited up to 1000."),
|
||||||
|
t.li(null, "The executed queries have a max timeout of 3 minutes."),
|
||||||
|
t.li(null, "The data is returned as byte strings without any additional formatting."),
|
||||||
|
t.li(null, "Multiple queries are supported but only the result of the last one is returned."),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// error alert
|
||||||
|
t.div(
|
||||||
|
{
|
||||||
|
hidden: () => pageData.isExecuting || !pageData.errorMsg,
|
||||||
|
className: "alert danger m-b-sm",
|
||||||
|
},
|
||||||
|
t.pre(null, () => pageData.errorMsg),
|
||||||
|
),
|
||||||
|
// success alert
|
||||||
|
t.div(
|
||||||
|
{
|
||||||
|
hidden: () =>
|
||||||
|
pageData.isExecuting || pageData.errorMsg || pageData.result?.columns?.length
|
||||||
|
|| app.utils.isEmpty(pageData.result),
|
||||||
|
className: "alert success m-b-sm",
|
||||||
|
},
|
||||||
|
t.p({ className: "txt-bold" }, "Query executed successfully!"),
|
||||||
|
t.p(null, "Affected rows: ", () => pageData.result?.affectedRows || 0),
|
||||||
|
),
|
||||||
|
// rows
|
||||||
|
t.div(
|
||||||
|
{
|
||||||
|
hidden: () => pageData.isExecuting || !pageData.result?.columns?.length,
|
||||||
|
className: "page-table-wrapper",
|
||||||
|
},
|
||||||
|
t.table(
|
||||||
|
{ className: "sql-console-table responsive-table optimize" },
|
||||||
|
t.thead(
|
||||||
|
{ className: "sticky" },
|
||||||
|
t.tr(null, () => {
|
||||||
|
return pageData.result?.columns?.map((col, i) => {
|
||||||
|
return t.th({
|
||||||
|
textContent: col.name,
|
||||||
|
className: () => {
|
||||||
|
let classes = "sort-handle";
|
||||||
|
|
||||||
|
if (pageData.sort?.index == i) {
|
||||||
|
classes += pageData.sort.asc ? " asc" : " desc";
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes;
|
||||||
|
},
|
||||||
|
onclick: () => toggleSort(i),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
t.tbody(
|
||||||
|
null,
|
||||||
|
() => {
|
||||||
|
if (!pageData.sortedResultRows.length) {
|
||||||
|
return t.tr(
|
||||||
|
null,
|
||||||
|
t.td(
|
||||||
|
{ colSpan: pageData.result?.columns?.length || 1, className: "txt-center" },
|
||||||
|
t.span({ className: "txt-hint" }, "No rows found."),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageData.sortedResultRows.map((rowData) => {
|
||||||
|
return t.tr(
|
||||||
|
null,
|
||||||
|
() => {
|
||||||
|
return pageData.result?.columns?.map((col, j) => {
|
||||||
|
const val = rowData[j];
|
||||||
|
return t.td(
|
||||||
|
{ "html-data-name": col.name },
|
||||||
|
val == null ? "NULL" : app.utils.truncate(val, 2000),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// load more btn
|
||||||
|
t.tr(
|
||||||
|
{
|
||||||
|
hidden: () => (
|
||||||
|
pageData.isExecuting
|
||||||
|
|| !pageData.result?.rows?.length
|
||||||
|
|| pageData.result.rows.length <= pageData.sortedResultRows.length
|
||||||
|
),
|
||||||
|
},
|
||||||
|
t.td(
|
||||||
|
{ colSpan: 99 },
|
||||||
|
t.button(
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
className: "btn lg secondary load-more-btn",
|
||||||
|
onclick: () => {
|
||||||
|
pageData.maxRows = pageData.result?.rows?.length || defaultMaxRows;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
t.span({
|
||||||
|
className: "txt",
|
||||||
|
textContent: () => `Load remaining (${pageData.totalRemainingRows})`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
t.footer(
|
||||||
|
{ className: "page-footer" },
|
||||||
|
t.span(
|
||||||
|
{
|
||||||
|
className: () => `exec-time ${pageData.isExecuting ? "faded" : ""}`,
|
||||||
|
},
|
||||||
|
"Time: ",
|
||||||
|
() => (pageData.result?.execTime || 0) + "ms",
|
||||||
|
),
|
||||||
|
t.span(
|
||||||
|
{
|
||||||
|
hidden: () => !pageData.result?.columns?.length,
|
||||||
|
className: () => `total-count ${pageData.isExecuting ? "faded" : ""}`,
|
||||||
|
},
|
||||||
|
"Rows: ",
|
||||||
|
() => pageData.result?.rows?.length || 0,
|
||||||
|
() => {
|
||||||
|
if (!pageData.result?.rows?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
" (",
|
||||||
|
t.span({
|
||||||
|
role: "button",
|
||||||
|
className: "link-hint",
|
||||||
|
textContent: "Export as CSV",
|
||||||
|
onclick: downloadCSV,
|
||||||
|
}),
|
||||||
|
")",
|
||||||
|
];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
app.components.credits(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
+8
-1
@@ -115,6 +115,13 @@ window.app.store = store({
|
|||||||
label: "Import collections",
|
label: "Import collections",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
Debug: [
|
||||||
|
{
|
||||||
|
href: "#/settings/sql",
|
||||||
|
icon: "ri-terminal-box-line",
|
||||||
|
label: "SQL console",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
predefinedAccentColors: [
|
predefinedAccentColors: [
|
||||||
@@ -205,7 +212,7 @@ window.app.store = store({
|
|||||||
addOrUpdateCollection(collection) {
|
addOrUpdateCollection(collection) {
|
||||||
const index = app.store.collections.findIndex((c) => c.id == collection.id);
|
const index = app.store.collections.findIndex((c) => c.id == collection.id);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
if (app.store.activeCollection.id == collection.id) {
|
if (app.store.activeCollection?.id == collection.id) {
|
||||||
app.store._activeCollectionIdOrName = collection.id;
|
app.store._activeCollectionIdOrName = collection.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -844,6 +844,29 @@ const utils = {
|
|||||||
utils.download(url, name);
|
utils.download(url, name);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a CSV file created from the provide array data.
|
||||||
|
*
|
||||||
|
* @param {Array} arr The JS array to serialize as CSV
|
||||||
|
* @param {string} name The result file name.
|
||||||
|
*/
|
||||||
|
downloadCSV(arr, name) {
|
||||||
|
name = name.endsWith(".csv") ? name : name + ".csv";
|
||||||
|
|
||||||
|
// seriaze data
|
||||||
|
let content = arr.map((row) => {
|
||||||
|
return row.map((v) => "\"" + ("" + v).replaceAll("\"", "\"\"") + "\"").join(",");
|
||||||
|
}).join("\n");
|
||||||
|
|
||||||
|
const blob = new Blob([content], {
|
||||||
|
type: "text/csv;charset=utf-8;",
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
utils.download(url, name);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a normalized API URL address that is used in the API example docs.
|
* Returns a normalized API URL address that is used in the API example docs.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user