mirror of
https://github.com/pocketbase/pocketbase.git
synced 2026-09-30 03:22:04 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9993aaa73 | ||
|
|
f77fb0cc1c | ||
|
|
460cc35bb6 | ||
|
|
f0bcffec8b | ||
|
|
c3844250e8 | ||
|
|
ebfbb55f91 | ||
|
|
d77479131a | ||
|
|
08b4fc20a9 | ||
|
|
9ec01d74d8 | ||
|
|
1679c88e6d | ||
|
|
91bbbc4bdb | ||
|
|
4ab9c6f87f | ||
|
|
68157a3a65 | ||
|
|
e6cf4ad2ef | ||
|
|
edd2eaae88 | ||
|
|
2e8e835a68 | ||
|
|
9cba6ac386 | ||
|
|
bd94940eef | ||
|
|
caf343ef9c | ||
|
|
d1bcaa65b1 | ||
|
|
2d60ad170e | ||
|
|
0ba963a5d7 | ||
|
|
87a6f1bebb | ||
|
|
aa5b5bb83e | ||
|
|
af193d440d | ||
|
|
7f06816008 | ||
|
|
e4a90f6605 | ||
|
|
acbce42bff | ||
|
|
b33bebd4cd | ||
|
|
3122223a71 | ||
|
|
dbbc1e25ca | ||
|
|
732044f795 | ||
|
|
9a1354ae62 | ||
|
|
e3876c0e13 | ||
|
|
b1307c9041 | ||
|
|
dcdf43f0fc | ||
|
|
f6a616b7e8 | ||
|
|
833a84b3d7 | ||
|
|
c72c951b24 | ||
|
|
e9969ed6d1 | ||
|
|
511259ed10 | ||
|
|
6b16b7856b | ||
|
|
231ddc9791 | ||
|
|
5bf2f692d8 | ||
|
|
ce28a9af78 | ||
|
|
db20e38cda | ||
|
|
286046e15a | ||
|
|
4440b5f817 | ||
|
|
d7745ba702 | ||
|
|
007ef1e152 | ||
|
|
a291cb5ca7 | ||
|
|
86049ed048 | ||
|
|
6457fc794f | ||
|
|
0eecbd8289 | ||
|
|
6521decfa8 | ||
|
|
c370d84074 | ||
|
|
4f5aa6ffda | ||
|
|
8921712821 | ||
|
|
1c63ae1324 | ||
|
|
cbaca91581 | ||
|
|
5551f8f5aa | ||
|
|
a56a04ed0e | ||
|
|
47bb0bc2d8 | ||
|
|
f49e90bd0b | ||
|
|
3820f3d7d7 | ||
|
|
faaf19d3bb | ||
|
|
6d0303deaf | ||
|
|
d36a044dc7 | ||
|
|
1cc67ef7e3 | ||
|
|
1e2d1045b8 | ||
|
|
df5a291d20 | ||
|
|
04e0ad9b21 | ||
|
|
24ab233376 | ||
|
|
a42ab6a205 | ||
|
|
472671fee1 | ||
|
|
6bde84131c | ||
|
|
e8b4a7eb26 | ||
|
|
3b0f60fe15 | ||
|
|
d3314e1e23 | ||
|
|
60eee96034 | ||
|
|
c6d5992442 | ||
|
|
a7f3805f87 | ||
|
|
92f21e3355 | ||
|
|
4c57968d89 | ||
|
|
b84ebabf4a | ||
|
|
90abe1612e | ||
|
|
dfabfa779e | ||
|
|
28c9f02cb4 | ||
|
|
67e1974228 | ||
|
|
0dcd332962 | ||
|
|
f70b457507 | ||
|
|
c8fd831115 | ||
|
|
beca0a044e | ||
|
|
9fa56b020c | ||
|
|
429929dd0c | ||
|
|
ecf7d657c0 | ||
|
|
4296148c87 | ||
|
|
bfb38ab51e | ||
|
|
1967dcfeba | ||
|
|
5c95e9b109 | ||
|
|
39c3a95a08 | ||
|
|
4944884683 | ||
|
|
c0a7d0f6c0 | ||
|
|
841a4b6913 | ||
|
|
7ce21cc4d9 | ||
|
|
7acd90498c | ||
|
|
f2a011d63f | ||
|
|
b31c2ceffa | ||
|
|
d8f9b52011 | ||
|
|
faa0a9f7dc | ||
|
|
4d94673839 | ||
|
|
025c9313e1 | ||
|
|
0b023b2c02 | ||
|
|
3358d8476b | ||
|
|
060ed8013e | ||
|
|
d69e72deaf |
No files matched your search
+151
-2
@@ -1,3 +1,152 @@
|
||||
## v0.16.9
|
||||
|
||||
- Register the `eagerRequestDataCache` middleware only for the internal `api` group routes to avoid conflicts with custom route handlers ([#2914](https://github.com/pocketbase/pocketbase/issues/2914)).
|
||||
|
||||
|
||||
## v0.16.8
|
||||
|
||||
- Fixed unique validator detailed error message not being returned when camelCase field name is used ([#2868](https://github.com/pocketbase/pocketbase/issues/2868)).
|
||||
|
||||
- Updated the index parser to allow no space between the table name and the columns list ([#2864](https://github.com/pocketbase/pocketbase/discussions/2864#discussioncomment-6373736)).
|
||||
|
||||
- Updated go deps.
|
||||
|
||||
|
||||
## v0.16.7
|
||||
|
||||
- Minor optimization for the list/search queries to use `rowid` with the `COUNT` statement when available.
|
||||
_This eliminates the temp B-TREE step when executing the query and for large datasets (eg. 150k) it could have 10x improvement (from ~580ms to ~60ms)._
|
||||
|
||||
|
||||
## v0.16.6
|
||||
|
||||
- Fixed collection index column sort normalization in the Admin UI ([#2681](https://github.com/pocketbase/pocketbase/pull/2681); thanks @SimonLoir).
|
||||
|
||||
- Removed unnecessary admins count in `apis.RequireAdminAuthOnlyIfAny()` middleware ([#2726](https://github.com/pocketbase/pocketbase/pull/2726); thanks @svekko).
|
||||
|
||||
- Fixed `multipart/form-data` request bind not populating map array values ([#2763](https://github.com/pocketbase/pocketbase/discussions/2763#discussioncomment-6278902)).
|
||||
|
||||
- Upgraded npm and Go dependencies.
|
||||
|
||||
|
||||
## v0.16.5
|
||||
|
||||
- Fixed the Admin UI serialization of implicit relation display fields ([#2675](https://github.com/pocketbase/pocketbase/issues/2675)).
|
||||
|
||||
- Reset the Admin UI sort in case the active sort collection field is renamed or deleted.
|
||||
|
||||
|
||||
## v0.16.4
|
||||
|
||||
- Fixed the selfupdate command not working on Windows due to missing `.exe` in the extracted binary path ([#2589](https://github.com/pocketbase/pocketbase/discussions/2589)).
|
||||
_Note that the command on Windows will work on v0.16.4+ onwards, meaning that you still will have to update manually one more time to v0.16.4._
|
||||
|
||||
- Added `int64`, `int32`, `uint`, `uint64` and `uint32` support when scanning `types.DateTime` ([#2602](https://github.com/pocketbase/pocketbase/discussions/2602))
|
||||
|
||||
- Updated dependencies.
|
||||
|
||||
|
||||
## v0.16.3
|
||||
|
||||
- Fixed schema fields sort not working on Safari/Gnome Web ([#2567](https://github.com/pocketbase/pocketbase/issues/2567)).
|
||||
|
||||
- Fixed default `PRAGMA`s not being applied for new connections ([#2570](https://github.com/pocketbase/pocketbase/discussions/2570)).
|
||||
|
||||
|
||||
## v0.16.2
|
||||
|
||||
- Fixed backups archive not excluding the local `backups` directory on Windows ([#2548](https://github.com/pocketbase/pocketbase/discussions/2548#discussioncomment-5979712)).
|
||||
|
||||
- Changed file field to not use `dataTransfer.effectAllowed` when dropping files since it is not reliable and consistent across different OS and browsers ([#2541](https://github.com/pocketbase/pocketbase/issues/2541)).
|
||||
|
||||
- Auto register the initial generated snapshot migration to prevent incorrectly reapplying the snapshot on Docker restart ([#2551](https://github.com/pocketbase/pocketbase/discussions/2551)).
|
||||
|
||||
- Fixed missing view id field error message typo.
|
||||
|
||||
|
||||
## v0.16.1
|
||||
|
||||
- Fixed backup restore not working in a container environment when `pb_data` is mounted as volume ([#2519](https://github.com/pocketbase/pocketbase/issues/2519)).
|
||||
|
||||
- Fixed Dart SDK realtime API preview example ([#2523](https://github.com/pocketbase/pocketbase/pull/2523); thanks @xFrann).
|
||||
|
||||
- Fixed typo in the backups create panel ([#2526](https://github.com/pocketbase/pocketbase/pull/2526); thanks @dschissler).
|
||||
|
||||
- Removed unnecessary slice length check in `list.ExistInSlice` ([#2527](https://github.com/pocketbase/pocketbase/pull/2527); thanks @KunalSin9h).
|
||||
|
||||
- Avoid mutating the cached request data on OAuth2 user create ([#2535](https://github.com/pocketbase/pocketbase/discussions/2535)).
|
||||
|
||||
- Fixed Export Collections "Download as JSON" ([#2540](https://github.com/pocketbase/pocketbase/issues/2540)).
|
||||
|
||||
- Fixed file field drag and drop not working in Firefox and Safari ([#2541](https://github.com/pocketbase/pocketbase/issues/2541)).
|
||||
|
||||
|
||||
## v0.16.0
|
||||
|
||||
- Added automated backups (_+ cron rotation_) APIs and UI for the `pb_data` directory.
|
||||
The backups can be also initialized programmatically using `app.CreateBackup("backup.zip")`.
|
||||
There is also experimental restore method - `app.RestoreBackup("backup.zip")` (_currently works only on UNIX systems as it relies on execve_).
|
||||
The backups can be stored locally or in external S3 storage (_it has its own configuration, separate from the file uploads storage filesystem_).
|
||||
|
||||
- Added option to limit the returned API fields using the `?fields` query parameter.
|
||||
The "fields picker" is applied for `SearchResult.Items` and every other JSON response. For example:
|
||||
```js
|
||||
// original: {"id": "RECORD_ID", "name": "abc", "description": "...something very big...", "items": ["id1", "id2"], "expand": {"items": [{"id": "id1", "name": "test1"}, {"id": "id2", "name": "test2"}]}}
|
||||
// output: {"name": "abc", "expand": {"items": [{"name": "test1"}, {"name": "test2"}]}}
|
||||
const result = await pb.collection("example").getOne("RECORD_ID", {
|
||||
expand: "items",
|
||||
fields: "name,expand.items.name",
|
||||
})
|
||||
```
|
||||
|
||||
- Added new `./pocketbase update` command to selfupdate the prebuilt executable (with option to generate a backup of your `pb_data`).
|
||||
|
||||
- Added new `./pocketbase admin` console command:
|
||||
```sh
|
||||
// creates new admin account
|
||||
./pocketbase admin create test@example.com 123456890
|
||||
|
||||
// changes the password of an existing admin account
|
||||
./pocketbase admin update test@example.com 0987654321
|
||||
|
||||
// deletes single admin account (if exists)
|
||||
./pocketbase admin delete test@example.com
|
||||
```
|
||||
|
||||
- Added `apis.Serve(app, options)` helper to allow starting the API server programmatically.
|
||||
|
||||
- Updated the schema fields Admin UI for "tidier" fields visualization.
|
||||
|
||||
- Updated the logs "real" user IP to check for `Fly-Client-IP` header and changed the `X-Forward-For` header to use the first non-empty leftmost-ish IP as it the closest to the "real IP".
|
||||
|
||||
- Added new `tools/archive` helper subpackage for managing archives (_currently works only with zip_).
|
||||
|
||||
- Added new `tools/cron` helper subpackage for scheduling task using cron-like syntax (_this eventually may get exported in the future in a separate repo_).
|
||||
|
||||
- Added new `Filesystem.List(prefix)` helper to retrieve a flat list with all files under the provided prefix.
|
||||
|
||||
- Added new `App.NewBackupsFilesystem()` helper to create a dedicated filesystem abstraction for managing app data backups.
|
||||
|
||||
- Added new `App.OnTerminate()` hook (_executed right before app termination, eg. on `SIGTERM` signal_).
|
||||
|
||||
- Added `accept` file field attribute with the field MIME types ([#2466](https://github.com/pocketbase/pocketbase/pull/2466); thanks @Nikhil1920).
|
||||
|
||||
- Added support for multiple files sort in the Admin UI ([#2445](https://github.com/pocketbase/pocketbase/issues/2445)).
|
||||
|
||||
- Added support for multiple relations sort in the Admin UI.
|
||||
|
||||
- Added `meta.isNew` to the OAuth2 auth JSON response to indicate a newly OAuth2 created PocketBase user.
|
||||
|
||||
|
||||
## v0.15.3
|
||||
|
||||
- Updated the Admin UI to use the latest JS SDK to resolve the `isNew` record field conflict ([#2385](https://github.com/pocketbase/pocketbase/discussions/2385)).
|
||||
|
||||
- Fixed `editor` field fullscreen `z-index` ([#2410](https://github.com/pocketbase/pocketbase/issues/2410)).
|
||||
|
||||
- Inserts the default app settings as part of the system init migration so that they are always available when accessed from within a user defined migration ([#2423](https://github.com/pocketbase/pocketbase/discussions/2423)).
|
||||
|
||||
|
||||
## v0.15.2
|
||||
|
||||
- Fixed View query `SELECT DISTINCT` identifiers parsing ([#2349-5706019](https://github.com/pocketbase/pocketbase/discussions/2349#discussioncomment-5706019)).
|
||||
@@ -26,13 +175,13 @@
|
||||
|
||||
- Simplified the OAuth2 authentication flow in a single "all in one" call ([#55](https://github.com/pocketbase/pocketbase/issues/55)).
|
||||
Requires JS SDK v0.14.0+ or Dart SDK v0.9.0+.
|
||||
The manual code-token exchange flow is still supported but the SDK methods is renamed to `authWithOAuth2Code()` (_to minimize the breaking changes the JS SDK has a function overload that will proxy the existing `authWithOauth2` calls to `authWithOAuth2Code`_).
|
||||
The manual code-token exchange flow is still supported but the SDK method is renamed to `authWithOAuth2Code()` (_to minimize the breaking changes the JS SDK has a function overload that will proxy the existing `authWithOauth2` calls to `authWithOAuth2Code`_).
|
||||
For more details and example, you could check https://pocketbase.io/docs/authentication/#oauth2-integration.
|
||||
|
||||
- Added support for protected files ([#215](https://github.com/pocketbase/pocketbase/issues/215)).
|
||||
Requires JS SDK v0.14.0+ or Dart SDK v0.9.0+.
|
||||
It works with a short lived (~5min) file token passed as query param with the file url.
|
||||
For more details and example, you could check https://pocketbase.io/docs/files-handling/#private-files.
|
||||
For more details and example, you could check https://pocketbase.io/docs/files-handling/#protected-files.
|
||||
|
||||
- **!** Fixed typo in `Record.WithUnkownData()` -> `Record.WithUnknownData()`.
|
||||
|
||||
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/forms"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// bindBackupApi registers the file api endpoints and the corresponding handlers.
|
||||
//
|
||||
// @todo add hooks once the app hooks api restructuring is finalized
|
||||
func bindBackupApi(app core.App, rg *echo.Group) {
|
||||
api := backupApi{app: app}
|
||||
|
||||
subGroup := rg.Group("/backups", ActivityLogger(app))
|
||||
subGroup.GET("", api.list, RequireAdminAuth())
|
||||
subGroup.POST("", api.create, RequireAdminAuth())
|
||||
subGroup.GET("/:key", api.download)
|
||||
subGroup.DELETE("/:key", api.delete, RequireAdminAuth())
|
||||
subGroup.POST("/:key/restore", api.restore, RequireAdminAuth())
|
||||
}
|
||||
|
||||
type backupApi struct {
|
||||
app core.App
|
||||
}
|
||||
|
||||
func (api *backupApi) list(c echo.Context) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
fsys, err := api.app.NewBackupsFilesystem()
|
||||
if err != nil {
|
||||
return NewBadRequestError("Failed to load backups filesystem.", err)
|
||||
}
|
||||
defer fsys.Close()
|
||||
|
||||
fsys.SetContext(ctx)
|
||||
|
||||
backups, err := fsys.List("")
|
||||
if err != nil {
|
||||
return NewBadRequestError("Failed to retrieve backup items. Raw error: \n"+err.Error(), nil)
|
||||
}
|
||||
|
||||
result := make([]models.BackupFileInfo, len(backups))
|
||||
|
||||
for i, obj := range backups {
|
||||
modified, _ := types.ParseDateTime(obj.ModTime)
|
||||
|
||||
result[i] = models.BackupFileInfo{
|
||||
Key: obj.Key,
|
||||
Size: obj.Size,
|
||||
Modified: modified,
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (api *backupApi) create(c echo.Context) error {
|
||||
if api.app.Cache().Has(core.CacheKeyActiveBackup) {
|
||||
return NewBadRequestError("Try again later - another backup/restore process has already been started", nil)
|
||||
}
|
||||
|
||||
form := forms.NewBackupCreate(api.app)
|
||||
if err := c.Bind(form); err != nil {
|
||||
return NewBadRequestError("An error occurred while loading the submitted data.", err)
|
||||
}
|
||||
|
||||
return form.Submit(func(next forms.InterceptorNextFunc[string]) forms.InterceptorNextFunc[string] {
|
||||
return func(name string) error {
|
||||
if err := next(name); err != nil {
|
||||
return NewBadRequestError("Failed to create backup.", err)
|
||||
}
|
||||
|
||||
// we don't retrieve the generated backup file because it may not be
|
||||
// available yet due to the eventually consistent nature of some S3 providers
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (api *backupApi) download(c echo.Context) error {
|
||||
fileToken := c.QueryParam("token")
|
||||
|
||||
_, err := api.app.Dao().FindAdminByToken(
|
||||
fileToken,
|
||||
api.app.Settings().AdminFileToken.Secret,
|
||||
)
|
||||
if err != nil {
|
||||
return NewForbiddenError("Insufficient permissions to access the resource.", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
fsys, err := api.app.NewBackupsFilesystem()
|
||||
if err != nil {
|
||||
return NewBadRequestError("Failed to load backups filesystem.", err)
|
||||
}
|
||||
defer fsys.Close()
|
||||
|
||||
fsys.SetContext(ctx)
|
||||
|
||||
key := c.PathParam("key")
|
||||
|
||||
br, err := fsys.GetFile(key)
|
||||
if err != nil {
|
||||
return NewBadRequestError("Failed to retrieve backup item. Raw error: \n"+err.Error(), nil)
|
||||
}
|
||||
defer br.Close()
|
||||
|
||||
return fsys.Serve(
|
||||
c.Response(),
|
||||
c.Request(),
|
||||
key,
|
||||
filepath.Base(key), // without the path prefix (if any)
|
||||
)
|
||||
}
|
||||
|
||||
func (api *backupApi) restore(c echo.Context) error {
|
||||
if api.app.Cache().Has(core.CacheKeyActiveBackup) {
|
||||
return NewBadRequestError("Try again later - another backup/restore process has already been started.", nil)
|
||||
}
|
||||
|
||||
// @todo remove the extra unescape after https://github.com/labstack/echo/issues/2447
|
||||
key, _ := url.PathUnescape(c.PathParam("key"))
|
||||
|
||||
existsCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
fsys, err := api.app.NewBackupsFilesystem()
|
||||
if err != nil {
|
||||
return NewBadRequestError("Failed to load backups filesystem.", err)
|
||||
}
|
||||
defer fsys.Close()
|
||||
|
||||
fsys.SetContext(existsCtx)
|
||||
|
||||
if exists, err := fsys.Exists(key); !exists {
|
||||
return NewBadRequestError("Missing or invalid backup file.", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
// wait max 15 minutes to fetch the backup
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// give some optimistic time to write the response
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
if err := api.app.RestoreBackup(ctx, key); err != nil && api.app.IsDebug() {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (api *backupApi) delete(c echo.Context) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
fsys, err := api.app.NewBackupsFilesystem()
|
||||
if err != nil {
|
||||
return NewBadRequestError("Failed to load backups filesystem.", err)
|
||||
}
|
||||
defer fsys.Close()
|
||||
|
||||
fsys.SetContext(ctx)
|
||||
|
||||
key := c.PathParam("key")
|
||||
|
||||
if key != "" && cast.ToString(api.app.Cache().Get(core.CacheKeyActiveBackup)) == key {
|
||||
return NewBadRequestError("The backup is currently being used and cannot be deleted.", nil)
|
||||
}
|
||||
|
||||
if err := fsys.Delete(key); err != nil {
|
||||
return NewBadRequestError("Invalid or already deleted backup file. Raw error: \n"+err.Error(), nil)
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
package apis_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"gocloud.dev/blob"
|
||||
)
|
||||
|
||||
func TestBackupsList(t *testing.T) {
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
Name: "unauthorized",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/backups",
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as auth record",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/backups",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
|
||||
},
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin (empty list)",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/backups",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`[]`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/backups",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"test1.zip"`,
|
||||
`"test2.zip"`,
|
||||
`"test3.zip"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupsCreate(t *testing.T) {
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
Name: "unauthorized",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/backups",
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
ensureNoBackups(t, app)
|
||||
},
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as auth record",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/backups",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
|
||||
},
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
ensureNoBackups(t, app)
|
||||
},
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin (pending backup)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/backups",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
app.Cache().Set(core.CacheKeyActiveBackup, "")
|
||||
},
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
ensureNoBackups(t, app)
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin (autogenerated name)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/backups",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
files, err := getBackupFiles(app)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if total := len(files); total != 1 {
|
||||
t.Fatalf("Expected 1 backup file, got %d", total)
|
||||
}
|
||||
|
||||
expected := "pb_backup_"
|
||||
if !strings.HasPrefix(files[0].Key, expected) {
|
||||
t.Fatalf("Expected backup file with prefix %q, got %q", expected, files[0].Key)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 204,
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin (invalid name)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/backups",
|
||||
Body: strings.NewReader(`{"name":"!test.zip"}`),
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
ensureNoBackups(t, app)
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{
|
||||
`"data":{`,
|
||||
`"name":{"code":"validation_match_invalid"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin (valid name)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/backups",
|
||||
Body: strings.NewReader(`{"name":"test.zip"}`),
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
files, err := getBackupFiles(app)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if total := len(files); total != 1 {
|
||||
t.Fatalf("Expected 1 backup file, got %d", total)
|
||||
}
|
||||
|
||||
expected := "test.zip"
|
||||
if files[0].Key != expected {
|
||||
t.Fatalf("Expected backup file %q, got %q", expected, files[0].Key)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 204,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupsDownload(t *testing.T) {
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
Name: "unauthorized",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/backups/test1.zip",
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 403,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "with record auth header",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/backups/test1.zip",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
|
||||
},
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 403,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "with admin auth header",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/backups/test1.zip",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 403,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "with empty or invalid token",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/backups/test1.zip?token=",
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 403,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "with valid record auth token",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/backups/test1.zip?token=eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 403,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "with valid record file token",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/backups/test1.zip?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsImV4cCI6MTg5MzQ1MjQ2MSwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwidHlwZSI6ImF1dGhSZWNvcmQifQ.0d_0EO6kfn9ijZIQWAqgRi8Bo1z7MKcg1LQpXhQsEPk",
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 403,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "with valid admin auth token",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/backups/test1.zip?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 403,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "with expired admin file token",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/backups/test1.zip?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MTY0MDk5MTY2MSwidHlwZSI6ImFkbWluIn0.g7Q_3UX6H--JWJ7yt1Hoe-1ugTX1KpbKzdt0zjGSe-E",
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 403,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "with valid admin file token but missing backup name",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/backups/mizzing?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MTg5MzQ1MjQ2MSwidHlwZSI6ImFkbWluIn0.LyAMpSfaHVsuUqIlqqEbhDQSdFzoPz_EIDcb2VJMBsU",
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "with valid admin file token",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/backups/test1.zip?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MTg5MzQ1MjQ2MSwidHlwZSI6ImFkbWluIn0.LyAMpSfaHVsuUqIlqqEbhDQSdFzoPz_EIDcb2VJMBsU",
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`storage/`,
|
||||
`data.db`,
|
||||
`logs.db`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupsDelete(t *testing.T) {
|
||||
noTestBackupFilesChanges := func(t *testing.T, app *tests.TestApp) {
|
||||
files, err := getBackupFiles(app)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := 3
|
||||
if total := len(files); total != expected {
|
||||
t.Fatalf("Expected %d backup(s), got %d", expected, total)
|
||||
}
|
||||
}
|
||||
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
Name: "unauthorized",
|
||||
Method: http.MethodDelete,
|
||||
Url: "/api/backups/test1.zip",
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
noTestBackupFilesChanges(t, app)
|
||||
},
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as auth record",
|
||||
Method: http.MethodDelete,
|
||||
Url: "/api/backups/test1.zip",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
|
||||
},
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
noTestBackupFilesChanges(t, app)
|
||||
},
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin (missing file)",
|
||||
Method: http.MethodDelete,
|
||||
Url: "/api/backups/missing.zip",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
noTestBackupFilesChanges(t, app)
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin (existing file with matching active backup)",
|
||||
Method: http.MethodDelete,
|
||||
Url: "/api/backups/test1.zip",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// mock active backup with the same name to delete
|
||||
app.Cache().Set(core.CacheKeyActiveBackup, "test1.zip")
|
||||
|
||||
},
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
noTestBackupFilesChanges(t, app)
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin (existing file and no matching active backup)",
|
||||
Method: http.MethodDelete,
|
||||
Url: "/api/backups/test1.zip",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// mock active backup with different name
|
||||
app.Cache().Set(core.CacheKeyActiveBackup, "new.zip")
|
||||
},
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
files, err := getBackupFiles(app)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if total := len(files); total != 2 {
|
||||
t.Fatalf("Expected 2 backup files, got %d", total)
|
||||
}
|
||||
|
||||
deletedFile := "test1.zip"
|
||||
|
||||
for _, f := range files {
|
||||
if f.Key == deletedFile {
|
||||
t.Fatalf("Expected backup %q to be deleted", deletedFile)
|
||||
}
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 204,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupsRestore(t *testing.T) {
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
Name: "unauthorized",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/backups/test1.zip/restore",
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as auth record",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/backups/test1.zip/restore",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
|
||||
},
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin (missing file)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/backups/missing.zip/restore",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin (active backup process)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/backups/test1.zip/restore",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
app.Cache().Set(core.CacheKeyActiveBackup, "")
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
func createTestBackups(app core.App) error {
|
||||
ctx := context.Background()
|
||||
|
||||
if err := app.CreateBackup(ctx, "test1.zip"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := app.CreateBackup(ctx, "test2.zip"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := app.CreateBackup(ctx, "test3.zip"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getBackupFiles(app core.App) ([]*blob.ListObject, error) {
|
||||
fsys, err := app.NewBackupsFilesystem()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fsys.Close()
|
||||
|
||||
return fsys.List("")
|
||||
}
|
||||
|
||||
func ensureNoBackups(t *testing.T, app *tests.TestApp) {
|
||||
files, err := getBackupFiles(app)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if total := len(files); total != 0 {
|
||||
t.Fatalf("Expected 0 backup files, got %d", total)
|
||||
}
|
||||
}
|
||||
+7
-6
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/labstack/echo/v5/middleware"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/rest"
|
||||
"github.com/pocketbase/pocketbase/ui"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
@@ -25,6 +26,9 @@ const trailedAdminPath = "/_/"
|
||||
func InitApi(app core.App) (*echo.Echo, error) {
|
||||
e := echo.New()
|
||||
e.Debug = app.IsDebug()
|
||||
e.JSONSerializer = &rest.Serializer{
|
||||
FieldsParam: "fields",
|
||||
}
|
||||
|
||||
// configure a custom router
|
||||
e.ResetRouterCreator(func(ec *echo.Echo) echo.Router {
|
||||
@@ -100,7 +104,7 @@ func InitApi(app core.App) (*echo.Echo, error) {
|
||||
bindStaticAdminUI(app, e)
|
||||
|
||||
// default routes
|
||||
api := e.Group("/api")
|
||||
api := e.Group("/api", eagerRequestDataCache(app))
|
||||
bindSettingsApi(app, api)
|
||||
bindAdminApi(app, api)
|
||||
bindCollectionApi(app, api)
|
||||
@@ -110,6 +114,7 @@ func InitApi(app core.App) (*echo.Echo, error) {
|
||||
bindRealtimeApi(app, api)
|
||||
bindLogsApi(app, api)
|
||||
bindHealthApi(app, api)
|
||||
bindBackupApi(app, api)
|
||||
|
||||
// trigger the custom BeforeServe hook for the created api router
|
||||
// allowing users to further adjust its options or register new routes
|
||||
@@ -121,10 +126,6 @@ func InitApi(app core.App) (*echo.Echo, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// note: it is after the OnBeforeServe hook to ensure that the implicit
|
||||
// cache is after any user custom defined middlewares
|
||||
e.Use(eagerRequestDataCache(app))
|
||||
|
||||
// catch all any route
|
||||
api.Any("/*", func(c echo.Context) error {
|
||||
return echo.ErrNotFound
|
||||
@@ -187,7 +188,7 @@ func bindStaticAdminUI(app core.App, e *echo.Echo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const totalAdminsCacheKey = "totalAdmins"
|
||||
const totalAdminsCacheKey = "@totalAdmins"
|
||||
|
||||
func updateTotalAdminsCache(app core.App) error {
|
||||
total, err := app.Dao().TotalAdmins()
|
||||
|
||||
+64
-41
@@ -214,15 +214,16 @@ func TestRemoveTrailingSlashMiddleware(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEagerRequestDataCache(t *testing.T) {
|
||||
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
Name: "[UNKNOWN] unsupported eager cached request method",
|
||||
Method: "UNKNOWN",
|
||||
Name: "custom non-api group route",
|
||||
Method: "POST",
|
||||
Url: "/custom",
|
||||
Body: strings.NewReader(`{"name":"test123"}`),
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: "UNKNOWN",
|
||||
Method: "POST",
|
||||
Path: "/custom",
|
||||
Handler: func(c echo.Context) error {
|
||||
data := &struct {
|
||||
@@ -240,52 +241,74 @@ func TestEagerRequestDataCache(t *testing.T) {
|
||||
t.Fatalf("Expected empty request data body, got, %v", r.Data)
|
||||
}
|
||||
|
||||
return c.String(200, data.Name)
|
||||
return c.NoContent(200)
|
||||
},
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"test123"},
|
||||
ExpectedStatus: 200,
|
||||
},
|
||||
}
|
||||
{
|
||||
Name: "api group route with unsupported eager cache request method",
|
||||
Method: "GET",
|
||||
Url: "/api/admins",
|
||||
Body: strings.NewReader(`{"name":"test123"}`),
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
// it is not important whether the route handler return an error since
|
||||
// we just need to ensure that the eagerRequestDataCache was registered
|
||||
next(c)
|
||||
|
||||
// supported eager cache request methods
|
||||
supportedMethods := []string{"POST", "PUT", "PATCH", "DELETE"}
|
||||
for _, m := range supportedMethods {
|
||||
scenarios = append(
|
||||
scenarios,
|
||||
tests.ApiScenario{
|
||||
Name: fmt.Sprintf("[%s] valid cached json body request", m),
|
||||
Method: http.MethodPost,
|
||||
Url: "/custom",
|
||||
Body: strings.NewReader(`{"name":"test123"}`),
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/custom",
|
||||
Handler: func(c echo.Context) error {
|
||||
data := &struct {
|
||||
Name string `json:"name"`
|
||||
}{}
|
||||
// ensure that the body was read at least once
|
||||
data := &struct {
|
||||
Name string `json:"name"`
|
||||
}{}
|
||||
c.Bind(data)
|
||||
|
||||
if err := c.Bind(data); err != nil {
|
||||
return err
|
||||
}
|
||||
// since the unknown method is not eager cache support
|
||||
// it should fail reading the json body twice
|
||||
r := apis.RequestData(c)
|
||||
if v := cast.ToString(r.Data["name"]); v != "" {
|
||||
t.Fatalf("Expected empty request data body, got, %v", r.Data)
|
||||
}
|
||||
|
||||
// try to read the body again
|
||||
r := apis.RequestData(c)
|
||||
if v := cast.ToString(r.Data["name"]); v != "test123" {
|
||||
t.Fatalf("Expected request data with name %q, got, %q", "test123", v)
|
||||
}
|
||||
|
||||
return c.String(200, data.Name)
|
||||
},
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"test123"},
|
||||
return nil
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
ExpectedStatus: 200,
|
||||
},
|
||||
{
|
||||
Name: "api group route with supported eager cache request method",
|
||||
Method: "POST",
|
||||
Url: "/api/admins",
|
||||
Body: strings.NewReader(`{"name":"test123"}`),
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
// it is not important whether the route handler return an error since
|
||||
// we just need to ensure that the eagerRequestDataCache was registered
|
||||
next(c)
|
||||
|
||||
// ensure that the body was read at least once
|
||||
data := &struct {
|
||||
Name string `json:"name"`
|
||||
}{}
|
||||
c.Bind(data)
|
||||
|
||||
// try to read the body again
|
||||
r := apis.RequestData(c)
|
||||
fmt.Println(r)
|
||||
if v := cast.ToString(r.Data["name"]); v != "test123" {
|
||||
t.Fatalf("Expected request data with name %q, got, %q", "test123", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
|
||||
+13
-5
@@ -19,12 +19,20 @@ type healthApi struct {
|
||||
app core.App
|
||||
}
|
||||
|
||||
type healthCheckResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
CanBackup bool `json:"canBackup"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// healthCheck returns a 200 OK response if the server is healthy.
|
||||
func (api *healthApi) healthCheck(c echo.Context) error {
|
||||
payload := map[string]any{
|
||||
"code": http.StatusOK,
|
||||
"message": "API is healthy.",
|
||||
}
|
||||
resp := new(healthCheckResponse)
|
||||
resp.Code = http.StatusOK
|
||||
resp.Message = "API is healthy."
|
||||
resp.Data.CanBackup = !api.app.Cache().Has(core.CacheKeyActiveBackup)
|
||||
|
||||
return c.JSON(http.StatusOK, payload)
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
@@ -16,6 +16,8 @@ func TestHealthAPI(t *testing.T) {
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"code":200`,
|
||||
`"data":{`,
|
||||
`"canBackup":true`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+13
-6
@@ -129,14 +129,17 @@ func RequireAdminAuth() echo.MiddlewareFunc {
|
||||
func RequireAdminAuthOnlyIfAny(app core.App) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
|
||||
if admin != nil {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
totalAdmins, err := app.Dao().TotalAdmins()
|
||||
if err != nil {
|
||||
return NewBadRequestError("Failed to fetch admins info.", err)
|
||||
}
|
||||
|
||||
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
|
||||
|
||||
if admin != nil || totalAdmins == 0 {
|
||||
if totalAdmins == 0 {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
@@ -368,15 +371,19 @@ func realUserIp(r *http.Request, fallbackIp string) string {
|
||||
return ip
|
||||
}
|
||||
|
||||
if ip := r.Header.Get("Fly-Client-IP"); ip != "" {
|
||||
return ip
|
||||
}
|
||||
|
||||
if ip := r.Header.Get("X-Real-IP"); ip != "" {
|
||||
return ip
|
||||
}
|
||||
|
||||
if ipsList := r.Header.Get("X-Forwarded-For"); ipsList != "" {
|
||||
// extract the first non-empty leftmost-ish ip
|
||||
ips := strings.Split(ipsList, ",")
|
||||
// extract the rightmost ip
|
||||
for i := len(ips) - 1; i >= 0; i-- {
|
||||
ip := strings.TrimSpace(ips[i])
|
||||
for _, ip := range ips {
|
||||
ip = strings.TrimSpace(ip)
|
||||
if ip != "" {
|
||||
return ip
|
||||
}
|
||||
|
||||
+13
-4
@@ -190,9 +190,10 @@ func (api *recordAuthApi) authWithOAuth2(c echo.Context) error {
|
||||
|
||||
form.SetBeforeNewRecordCreateFunc(func(createForm *forms.RecordUpsert, authRecord *models.Record, authUser *auth.AuthUser) error {
|
||||
return createForm.DrySubmit(func(txDao *daos.Dao) error {
|
||||
requestData := RequestData(c)
|
||||
requestData.Data = form.CreateData
|
||||
event.IsNewRecord = true
|
||||
// clone the current request data and assign the form create data as its body data
|
||||
requestData := *RequestData(c)
|
||||
requestData.Data = form.CreateData
|
||||
|
||||
createRuleFunc := func(q *dbx.SelectQuery) error {
|
||||
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
|
||||
@@ -205,7 +206,7 @@ func (api *recordAuthApi) authWithOAuth2(c echo.Context) error {
|
||||
}
|
||||
|
||||
if *collection.CreateRule != "" {
|
||||
resolver := resolvers.NewRecordFieldResolver(txDao, collection, requestData, true)
|
||||
resolver := resolvers.NewRecordFieldResolver(txDao, collection, &requestData, true)
|
||||
expr, err := search.FilterData(*collection.CreateRule).BuildExpr(resolver)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -242,7 +243,15 @@ func (api *recordAuthApi) authWithOAuth2(c echo.Context) error {
|
||||
e.Record = data.Record
|
||||
e.OAuth2User = data.OAuth2User
|
||||
|
||||
return RecordAuthResponse(api.app, e.HttpContext, e.Record, e.OAuth2User)
|
||||
meta := struct {
|
||||
*auth.AuthUser
|
||||
IsNew bool `json:"isNew"`
|
||||
}{
|
||||
AuthUser: e.OAuth2User,
|
||||
IsNew: event.IsNewRecord,
|
||||
}
|
||||
|
||||
return RecordAuthResponse(api.app, e.HttpContext, e.Record, meta)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -68,6 +68,11 @@ func (api *recordApi) list(c echo.Context) error {
|
||||
searchProvider := search.NewProvider(fieldsResolver).
|
||||
Query(api.app.Dao().RecordQuery(collection))
|
||||
|
||||
// views don't have "rowid" so we fallback to "id"
|
||||
if collection.IsView() {
|
||||
searchProvider.CountCol("id")
|
||||
}
|
||||
|
||||
if requestData.Admin == nil && collection.ListRule != nil {
|
||||
searchProvider.AddFilter(search.FilterData(*collection.ListRule))
|
||||
}
|
||||
|
||||
@@ -121,7 +121,9 @@ func EnrichRecords(c echo.Context, dao *daos.Dao, records []*models.Record, defa
|
||||
}
|
||||
|
||||
expands := defaultExpands
|
||||
expands = append(expands, strings.Split(c.QueryParam(expandQueryParam), ",")...)
|
||||
if param := c.QueryParam(expandQueryParam); param != "" {
|
||||
expands = append(expands, strings.Split(param, ",")...)
|
||||
}
|
||||
if len(expands) == 0 {
|
||||
return nil // nothing to expand
|
||||
}
|
||||
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/labstack/echo/v5/middleware"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/migrations/logs"
|
||||
"github.com/pocketbase/pocketbase/tools/migrate"
|
||||
"golang.org/x/crypto/acme"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
// ServeOptions defines an optional struct for apis.Serve().
|
||||
type ServeOptions struct {
|
||||
ShowStartBanner bool
|
||||
HttpAddr string
|
||||
HttpsAddr string
|
||||
AllowedOrigins []string // optional list of CORS origins (default to "*")
|
||||
BeforeServeFunc func(server *http.Server) error
|
||||
}
|
||||
|
||||
// Serve starts a new app web server.
|
||||
func Serve(app core.App, options *ServeOptions) error {
|
||||
if options == nil {
|
||||
options = &ServeOptions{}
|
||||
}
|
||||
|
||||
if len(options.AllowedOrigins) == 0 {
|
||||
options.AllowedOrigins = []string{"*"}
|
||||
}
|
||||
|
||||
// ensure that the latest migrations are applied before starting the server
|
||||
if err := runMigrations(app); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// reload app settings in case a new default value was set with a migration
|
||||
// (or if this is the first time the init migration was executed)
|
||||
if err := app.RefreshSettings(); err != nil {
|
||||
color.Yellow("=====================================")
|
||||
color.Yellow("WARNING: Settings load error! \n%v", err)
|
||||
color.Yellow("Fallback to the application defaults.")
|
||||
color.Yellow("=====================================")
|
||||
}
|
||||
|
||||
router, err := InitApi(app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// configure cors
|
||||
router.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||
Skipper: middleware.DefaultSkipper,
|
||||
AllowOrigins: options.AllowedOrigins,
|
||||
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
|
||||
}))
|
||||
|
||||
// start http server
|
||||
// ---
|
||||
mainAddr := options.HttpAddr
|
||||
if options.HttpsAddr != "" {
|
||||
mainAddr = options.HttpsAddr
|
||||
}
|
||||
|
||||
mainHost, _, _ := net.SplitHostPort(mainAddr)
|
||||
|
||||
certManager := autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
Cache: autocert.DirCache(filepath.Join(app.DataDir(), ".autocert_cache")),
|
||||
HostPolicy: autocert.HostWhitelist(mainHost, "www."+mainHost),
|
||||
}
|
||||
|
||||
serverConfig := &http.Server{
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: certManager.GetCertificate,
|
||||
NextProtos: []string{acme.ALPNProto},
|
||||
},
|
||||
ReadTimeout: 10 * time.Minute,
|
||||
ReadHeaderTimeout: 30 * time.Second,
|
||||
// WriteTimeout: 60 * time.Second, // breaks sse!
|
||||
Handler: router,
|
||||
Addr: mainAddr,
|
||||
}
|
||||
|
||||
if options.BeforeServeFunc != nil {
|
||||
if err := options.BeforeServeFunc(serverConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if options.ShowStartBanner {
|
||||
schema := "http"
|
||||
if options.HttpsAddr != "" {
|
||||
schema = "https"
|
||||
}
|
||||
|
||||
date := new(strings.Builder)
|
||||
log.New(date, "", log.LstdFlags).Print()
|
||||
|
||||
bold := color.New(color.Bold).Add(color.FgGreen)
|
||||
bold.Printf(
|
||||
"%s Server started at %s\n",
|
||||
strings.TrimSpace(date.String()),
|
||||
color.CyanString("%s://%s", schema, serverConfig.Addr),
|
||||
)
|
||||
|
||||
regular := color.New()
|
||||
regular.Printf(" ➜ REST API: %s\n", color.CyanString("%s://%s/api/", schema, serverConfig.Addr))
|
||||
regular.Printf(" ➜ Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, serverConfig.Addr))
|
||||
}
|
||||
|
||||
// try to gracefully shutdown the server on app termination
|
||||
app.OnTerminate().Add(func(e *core.TerminateEvent) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
serverConfig.Shutdown(ctx)
|
||||
return nil
|
||||
})
|
||||
|
||||
// start HTTPS server
|
||||
if options.HttpsAddr != "" {
|
||||
// if httpAddr is set, start an HTTP server to redirect the traffic to the HTTPS version
|
||||
if options.HttpAddr != "" {
|
||||
go http.ListenAndServe(options.HttpAddr, certManager.HTTPHandler(nil))
|
||||
}
|
||||
|
||||
return serverConfig.ListenAndServeTLS("", "")
|
||||
}
|
||||
|
||||
// OR start HTTP server
|
||||
return serverConfig.ListenAndServe()
|
||||
}
|
||||
|
||||
type migrationsConnection struct {
|
||||
DB *dbx.DB
|
||||
MigrationsList migrate.MigrationsList
|
||||
}
|
||||
|
||||
func runMigrations(app core.App) error {
|
||||
connections := []migrationsConnection{
|
||||
{
|
||||
DB: app.DB(),
|
||||
MigrationsList: migrations.AppMigrations,
|
||||
},
|
||||
{
|
||||
DB: app.LogsDB(),
|
||||
MigrationsList: logs.LogsMigrations,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range connections {
|
||||
runner, err := migrate.NewRunner(c.DB, c.MigrationsList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := runner.Up(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+13
-20
@@ -1,7 +1,6 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
@@ -10,7 +9,6 @@ import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/forms"
|
||||
"github.com/pocketbase/pocketbase/models/settings"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
// bindSettingsApi registers the settings api endpoints.
|
||||
@@ -86,27 +84,22 @@ func (api *settingsApi) set(c echo.Context) error {
|
||||
}
|
||||
|
||||
func (api *settingsApi) testS3(c echo.Context) error {
|
||||
if !api.app.Settings().S3.Enabled {
|
||||
return NewBadRequestError("S3 storage is not enabled.", nil)
|
||||
form := forms.NewTestS3Filesystem(api.app)
|
||||
|
||||
// load request
|
||||
if err := c.Bind(form); err != nil {
|
||||
return NewBadRequestError("An error occurred while loading the submitted data.", err)
|
||||
}
|
||||
|
||||
fs, err := api.app.NewFilesystem()
|
||||
if err != nil {
|
||||
return NewBadRequestError("Failed to initialize the S3 storage. Raw error: \n"+err.Error(), nil)
|
||||
}
|
||||
defer fs.Close()
|
||||
// send
|
||||
if err := form.Submit(); err != nil {
|
||||
// form error
|
||||
if fErr, ok := err.(validation.Errors); ok {
|
||||
return NewBadRequestError("Failed to test the S3 filesystem.", fErr)
|
||||
}
|
||||
|
||||
testPrefix := "pb_settings_test_" + security.PseudorandomString(5)
|
||||
testFileKey := testPrefix + "/test.txt"
|
||||
|
||||
// try to upload a test file
|
||||
if err := fs.Upload([]byte("test"), testFileKey); err != nil {
|
||||
return NewBadRequestError("Failed to upload a test file. Raw error: \n"+err.Error(), nil)
|
||||
}
|
||||
|
||||
// test prefix deletion (ensures that both bucket list and delete works)
|
||||
if errs := fs.DeletePrefix(testPrefix); len(errs) > 0 {
|
||||
return NewBadRequestError(fmt.Sprintf("Failed to delete a test file. Raw error: %v", errs), nil)
|
||||
// mailer error
|
||||
return NewBadRequestError("Failed to test the S3 filesystem. Raw error: \n"+err.Error(), nil)
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
|
||||
+36
-3
@@ -47,6 +47,7 @@ func TestSettingsList(t *testing.T) {
|
||||
`"logs":{`,
|
||||
`"smtp":{`,
|
||||
`"s3":{`,
|
||||
`"backups":{`,
|
||||
`"adminAuthToken":{`,
|
||||
`"adminPasswordResetToken":{`,
|
||||
`"adminFileToken":{`,
|
||||
@@ -125,6 +126,7 @@ func TestSettingsSet(t *testing.T) {
|
||||
`"logs":{`,
|
||||
`"smtp":{`,
|
||||
`"s3":{`,
|
||||
`"backups":{`,
|
||||
`"adminAuthToken":{`,
|
||||
`"adminPasswordResetToken":{`,
|
||||
`"adminFileToken":{`,
|
||||
@@ -190,6 +192,7 @@ func TestSettingsSet(t *testing.T) {
|
||||
`"logs":{`,
|
||||
`"smtp":{`,
|
||||
`"s3":{`,
|
||||
`"backups":{`,
|
||||
`"adminAuthToken":{`,
|
||||
`"adminPasswordResetToken":{`,
|
||||
`"adminFileToken":{`,
|
||||
@@ -255,14 +258,44 @@ func TestSettingsTestS3(t *testing.T) {
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin (no s3)",
|
||||
Name: "authorized as admin (missing body + no s3)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/settings/test/s3",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{
|
||||
`"data":{`,
|
||||
`"filesystem":{`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin (invalid filesystem)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/settings/test/s3",
|
||||
Body: strings.NewReader(`{"filesystem":"invalid"}`),
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{
|
||||
`"data":{`,
|
||||
`"filesystem":{`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin (valid filesystem and no s3)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/settings/test/s3",
|
||||
Body: strings.NewReader(`{"filesystem":"storage"}`),
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{
|
||||
`"data":{}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewAdminCommand creates and returns new command for managing
|
||||
// admin accounts (create, update, delete).
|
||||
func NewAdminCommand(app core.App) *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "admin",
|
||||
Short: "Manages admin accounts",
|
||||
}
|
||||
|
||||
command.AddCommand(adminCreateCommand(app))
|
||||
command.AddCommand(adminUpdateCommand(app))
|
||||
command.AddCommand(adminDeleteCommand(app))
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func adminCreateCommand(app core.App) *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "create",
|
||||
Example: "admin create test@example.com 1234567890",
|
||||
Short: "Creates a new admin account",
|
||||
// prevents printing the error log twice
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
if len(args) != 2 {
|
||||
return errors.New("Missing email and password arguments.")
|
||||
}
|
||||
|
||||
if args[0] == "" || is.EmailFormat.Validate(args[0]) != nil {
|
||||
return errors.New("Missing or invalid email address.")
|
||||
}
|
||||
|
||||
if len(args[1]) < 8 {
|
||||
return errors.New("The password must be at least 8 chars long.")
|
||||
}
|
||||
|
||||
admin := &models.Admin{}
|
||||
admin.Email = args[0]
|
||||
admin.SetPassword(args[1])
|
||||
|
||||
if err := app.Dao().SaveAdmin(admin); err != nil {
|
||||
return fmt.Errorf("Failed to create new admin account: %v", err)
|
||||
}
|
||||
|
||||
color.Green("Successfully created new admin %s!", admin.Email)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func adminUpdateCommand(app core.App) *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "update",
|
||||
Example: "admin update test@example.com 1234567890",
|
||||
Short: "Changes the password of a single admin account",
|
||||
// prevents printing the error log twice
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
if len(args) != 2 {
|
||||
return errors.New("Missing email and password arguments.")
|
||||
}
|
||||
|
||||
if args[0] == "" || is.EmailFormat.Validate(args[0]) != nil {
|
||||
return errors.New("Missing or invalid email address.")
|
||||
}
|
||||
|
||||
if len(args[1]) < 8 {
|
||||
return errors.New("The new password must be at least 8 chars long.")
|
||||
}
|
||||
|
||||
admin, err := app.Dao().FindAdminByEmail(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("Admin with email %s doesn't exist.", args[0])
|
||||
}
|
||||
|
||||
admin.SetPassword(args[1])
|
||||
|
||||
if err := app.Dao().SaveAdmin(admin); err != nil {
|
||||
return fmt.Errorf("Failed to change admin %s password: %v", admin.Email, err)
|
||||
}
|
||||
|
||||
color.Green("Successfully changed admin %s password!", admin.Email)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func adminDeleteCommand(app core.App) *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "delete",
|
||||
Example: "admin delete test@example.com",
|
||||
Short: "Deletes an existing admin account",
|
||||
// prevents printing the error log twice
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
if len(args) == 0 || args[0] == "" || is.EmailFormat.Validate(args[0]) != nil {
|
||||
return errors.New("Invalid or missing email address.")
|
||||
}
|
||||
|
||||
admin, err := app.Dao().FindAdminByEmail(args[0])
|
||||
if err != nil {
|
||||
color.Yellow("Admin %s is already deleted.", args[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := app.Dao().DeleteAdmin(admin); err != nil {
|
||||
return fmt.Errorf("Failed to delete admin %s: %v", admin.Email, err)
|
||||
}
|
||||
|
||||
color.Green("Successfully deleted admin %s!", admin.Email)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package cmd_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/cmd"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
)
|
||||
|
||||
func TestAdminCreateCommand(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
email string
|
||||
password string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
"empty email and password",
|
||||
"",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"empty email",
|
||||
"",
|
||||
"1234567890",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"invalid email",
|
||||
"invalid",
|
||||
"1234567890",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"duplicated email",
|
||||
"test@example.com",
|
||||
"1234567890",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"empty password",
|
||||
"test@example.com",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"short password",
|
||||
"test_new@example.com",
|
||||
"1234567",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"valid email and password",
|
||||
"test_new@example.com",
|
||||
"12345678",
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
command := cmd.NewAdminCommand(app)
|
||||
command.SetArgs([]string{"create", s.email, s.password})
|
||||
|
||||
err := command.Execute()
|
||||
|
||||
hasErr := err != nil
|
||||
if s.expectError != hasErr {
|
||||
t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, err)
|
||||
}
|
||||
|
||||
if hasErr {
|
||||
continue
|
||||
}
|
||||
|
||||
// check whether the admin account was actually created
|
||||
admin, err := app.Dao().FindAdminByEmail(s.email)
|
||||
if err != nil {
|
||||
t.Errorf("[%s] Failed to fetch created admin %s: %v", s.name, s.email, err)
|
||||
} else if !admin.ValidatePassword(s.password) {
|
||||
t.Errorf("[%s] Expected the admin password to match", s.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminUpdateCommand(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
email string
|
||||
password string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
"empty email and password",
|
||||
"",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"empty email",
|
||||
"",
|
||||
"1234567890",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"invalid email",
|
||||
"invalid",
|
||||
"1234567890",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"nonexisting admin",
|
||||
"test_missing@example.com",
|
||||
"1234567890",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"empty password",
|
||||
"test@example.com",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"short password",
|
||||
"test_new@example.com",
|
||||
"1234567",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"valid email and password",
|
||||
"test@example.com",
|
||||
"12345678",
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
command := cmd.NewAdminCommand(app)
|
||||
command.SetArgs([]string{"update", s.email, s.password})
|
||||
|
||||
err := command.Execute()
|
||||
|
||||
hasErr := err != nil
|
||||
if s.expectError != hasErr {
|
||||
t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, err)
|
||||
}
|
||||
|
||||
if hasErr {
|
||||
continue
|
||||
}
|
||||
|
||||
// check whether the admin password was actually changed
|
||||
admin, err := app.Dao().FindAdminByEmail(s.email)
|
||||
if err != nil {
|
||||
t.Errorf("[%s] Failed to fetch admin %s: %v", s.name, s.email, err)
|
||||
} else if !admin.ValidatePassword(s.password) {
|
||||
t.Errorf("[%s] Expected the admin password to match", s.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminDeleteCommand(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
email string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
"empty email",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"invalid email",
|
||||
"invalid",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"nonexisting admin",
|
||||
"test_missing@example.com",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"existing admin",
|
||||
"test@example.com",
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
command := cmd.NewAdminCommand(app)
|
||||
command.SetArgs([]string{"delete", s.email})
|
||||
|
||||
err := command.Execute()
|
||||
|
||||
hasErr := err != nil
|
||||
if s.expectError != hasErr {
|
||||
t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, err)
|
||||
}
|
||||
|
||||
if hasErr {
|
||||
continue
|
||||
}
|
||||
|
||||
// check whether the admin account was actually deleted
|
||||
if _, err := app.Dao().FindAdminByEmail(s.email); err == nil {
|
||||
t.Errorf("[%s] Expected the admin account to be deleted", s.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-133
@@ -1,25 +1,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/labstack/echo/v5/middleware"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/migrations/logs"
|
||||
"github.com/pocketbase/pocketbase/tools/migrate"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/acme"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
// NewServeCommand creates and returns new command responsible for
|
||||
@@ -33,96 +20,15 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command {
|
||||
Use: "serve",
|
||||
Short: "Starts the web server (default to 127.0.0.1:8090)",
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
// ensure that the latest migrations are applied before starting the server
|
||||
if err := runMigrations(app); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err := apis.Serve(app, &apis.ServeOptions{
|
||||
HttpAddr: httpAddr,
|
||||
HttpsAddr: httpsAddr,
|
||||
ShowStartBanner: showStartBanner,
|
||||
AllowedOrigins: allowedOrigins,
|
||||
})
|
||||
|
||||
// reload app settings in case a new default value was set with a migration
|
||||
// (or if this is the first time the init migration was executed)
|
||||
if err := app.RefreshSettings(); err != nil {
|
||||
color.Yellow("=====================================")
|
||||
color.Yellow("WARNING: Settings load error! \n%v", err)
|
||||
color.Yellow("Fallback to the application defaults.")
|
||||
color.Yellow("=====================================")
|
||||
}
|
||||
|
||||
router, err := apis.InitApi(app)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// configure cors
|
||||
router.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||
Skipper: middleware.DefaultSkipper,
|
||||
AllowOrigins: allowedOrigins,
|
||||
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
|
||||
}))
|
||||
|
||||
// start http server
|
||||
// ---
|
||||
mainAddr := httpAddr
|
||||
if httpsAddr != "" {
|
||||
mainAddr = httpsAddr
|
||||
}
|
||||
|
||||
mainHost, _, _ := net.SplitHostPort(mainAddr)
|
||||
|
||||
certManager := autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
Cache: autocert.DirCache(filepath.Join(app.DataDir(), ".autocert_cache")),
|
||||
HostPolicy: autocert.HostWhitelist(mainHost, "www."+mainHost),
|
||||
}
|
||||
|
||||
serverConfig := &http.Server{
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: certManager.GetCertificate,
|
||||
NextProtos: []string{acme.ALPNProto},
|
||||
},
|
||||
ReadTimeout: 5 * time.Minute,
|
||||
ReadHeaderTimeout: 30 * time.Second,
|
||||
// WriteTimeout: 60 * time.Second, // breaks sse!
|
||||
Handler: router,
|
||||
Addr: mainAddr,
|
||||
}
|
||||
|
||||
if showStartBanner {
|
||||
schema := "http"
|
||||
if httpsAddr != "" {
|
||||
schema = "https"
|
||||
}
|
||||
|
||||
date := new(strings.Builder)
|
||||
log.New(date, "", log.LstdFlags).Print()
|
||||
|
||||
bold := color.New(color.Bold).Add(color.FgGreen)
|
||||
bold.Printf(
|
||||
"%s Server started at %s\n",
|
||||
strings.TrimSpace(date.String()),
|
||||
color.CyanString("%s://%s", schema, serverConfig.Addr),
|
||||
)
|
||||
|
||||
regular := color.New()
|
||||
regular.Printf(" ➜ REST API: %s\n", color.CyanString("%s://%s/api/", schema, serverConfig.Addr))
|
||||
regular.Printf(" ➜ Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, serverConfig.Addr))
|
||||
}
|
||||
|
||||
var serveErr error
|
||||
if httpsAddr != "" {
|
||||
// if httpAddr is set, start an HTTP server to redirect the traffic to the HTTPS version
|
||||
if httpAddr != "" {
|
||||
go http.ListenAndServe(httpAddr, certManager.HTTPHandler(nil))
|
||||
}
|
||||
|
||||
// start HTTPS server
|
||||
serveErr = serverConfig.ListenAndServeTLS("", "")
|
||||
} else {
|
||||
// start HTTP server
|
||||
serveErr = serverConfig.ListenAndServe()
|
||||
}
|
||||
|
||||
if serveErr != http.ErrServerClosed {
|
||||
log.Fatalln(serveErr)
|
||||
if err != http.ErrServerClosed {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -150,34 +56,3 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command {
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
type migrationsConnection struct {
|
||||
DB *dbx.DB
|
||||
MigrationsList migrate.MigrationsList
|
||||
}
|
||||
|
||||
func runMigrations(app core.App) error {
|
||||
connections := []migrationsConnection{
|
||||
{
|
||||
DB: app.DB(),
|
||||
MigrationsList: migrations.AppMigrations,
|
||||
},
|
||||
{
|
||||
DB: app.LogsDB(),
|
||||
MigrationsList: logs.LogsMigrations,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range connections {
|
||||
runner, err := migrate.NewRunner(c.DB, c.MigrationsList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := runner.Up(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,444 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Temporary console command to update the pb_data structure to be compatible with the v0.8.0 changes.
|
||||
//
|
||||
// NB! It will be removed in v0.9+
|
||||
func NewTempUpgradeCommand(app core.App) *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "upgrade",
|
||||
Short: "Upgrades your existing pb_data to be compatible with the v0.8.x changes",
|
||||
Long: `
|
||||
Upgrades your existing pb_data to be compatible with the v0.8.x changes
|
||||
Prerequisites and caveats:
|
||||
- already upgraded to v0.7.*
|
||||
- no existing users collection
|
||||
- existing profiles collection fields like email, username, verified, etc. will be renamed to username2, email2, etc.
|
||||
`,
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
if err := upgrade(app); err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func upgrade(app core.App) error {
|
||||
if _, err := app.Dao().FindCollectionByNameOrId("users"); err == nil {
|
||||
return errors.New("It seems that you've already upgraded or have an existing 'users' collection.")
|
||||
}
|
||||
|
||||
return app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
|
||||
if err := migrateCollections(txDao); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := migrateUsers(app, txDao); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := resetMigrationsTable(txDao); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bold := color.New(color.Bold).Add(color.FgGreen)
|
||||
bold.Println("The pb_data upgrade completed successfully!")
|
||||
bold.Println("You can now start the application as usual with the 'serve' command.")
|
||||
bold.Println("Please review the migrated collection API rules and fields in the Admin UI and apply the necessary changes in your client-side code.")
|
||||
fmt.Println()
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
func migrateCollections(txDao *daos.Dao) error {
|
||||
// add new collection columns
|
||||
if _, err := txDao.DB().AddColumn("_collections", "type", "TEXT DEFAULT 'base' NOT NULL").Execute(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := txDao.DB().AddColumn("_collections", "options", "JSON DEFAULT '{}' NOT NULL").Execute(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ruleReplacements := []struct {
|
||||
old string
|
||||
new string
|
||||
}{
|
||||
{"expand", "expand2"},
|
||||
{"collecitonId", "collectionId2"},
|
||||
{"collecitonName", "collectionName2"},
|
||||
{"profile.userId", "profile.id"},
|
||||
|
||||
// @collection.*
|
||||
{"@collection.profiles.userId", "@collection.users.id"},
|
||||
{"@collection.profiles.username", "@collection.users.username2"},
|
||||
{"@collection.profiles.email", "@collection.users.email2"},
|
||||
{"@collection.profiles.emailVisibility", "@collection.users.emailVisibility2"},
|
||||
{"@collection.profiles.verified", "@collection.users.verified2"},
|
||||
{"@collection.profiles.tokenKey", "@collection.users.tokenKey2"},
|
||||
{"@collection.profiles.passwordHash", "@collection.users.passwordHash2"},
|
||||
{"@collection.profiles.lastResetSentAt", "@collection.users.lastResetSentAt2"},
|
||||
{"@collection.profiles.lastVerificationSentAt", "@collection.users.lastVerificationSentAt2"},
|
||||
{"@collection.profiles.", "@collection.users."},
|
||||
|
||||
// @request.*
|
||||
{"@request.user.profile.userId", "@request.auth.id"},
|
||||
{"@request.user.profile.username", "@request.auth.username2"},
|
||||
{"@request.user.profile.email", "@request.auth.email2"},
|
||||
{"@request.user.profile.emailVisibility", "@request.auth.emailVisibility2"},
|
||||
{"@request.user.profile.verified", "@request.auth.verified2"},
|
||||
{"@request.user.profile.tokenKey", "@request.auth.tokenKey2"},
|
||||
{"@request.user.profile.passwordHash", "@request.auth.passwordHash2"},
|
||||
{"@request.user.profile.lastResetSentAt", "@request.auth.lastResetSentAt2"},
|
||||
{"@request.user.profile.lastVerificationSentAt", "@request.auth.lastVerificationSentAt2"},
|
||||
{"@request.user.profile.", "@request.auth."},
|
||||
{"@request.user", "@request.auth"},
|
||||
}
|
||||
|
||||
collections := []*models.Collection{}
|
||||
if err := txDao.CollectionQuery().All(&collections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, collection := range collections {
|
||||
collection.Type = models.CollectionTypeBase
|
||||
collection.NormalizeOptions()
|
||||
|
||||
// rename profile fields
|
||||
// ---
|
||||
fieldsToRename := []string{
|
||||
"collectionId",
|
||||
"collectionName",
|
||||
"expand",
|
||||
}
|
||||
if collection.Name == "profiles" {
|
||||
fieldsToRename = append(fieldsToRename,
|
||||
"username",
|
||||
"email",
|
||||
"emailVisibility",
|
||||
"verified",
|
||||
"tokenKey",
|
||||
"passwordHash",
|
||||
"lastResetSentAt",
|
||||
"lastVerificationSentAt",
|
||||
)
|
||||
}
|
||||
for _, name := range fieldsToRename {
|
||||
f := collection.Schema.GetFieldByName(name)
|
||||
if f != nil {
|
||||
color.Blue("[%s - renamed field]", collection.Name)
|
||||
color.Yellow(" - old: %s", f.Name)
|
||||
color.Green(" - new: %s2", f.Name)
|
||||
fmt.Println()
|
||||
f.Name += "2"
|
||||
}
|
||||
}
|
||||
// ---
|
||||
|
||||
// replace rule fields
|
||||
// ---
|
||||
rules := map[string]*string{
|
||||
"ListRule": collection.ListRule,
|
||||
"ViewRule": collection.ViewRule,
|
||||
"CreateRule": collection.CreateRule,
|
||||
"UpdateRule": collection.UpdateRule,
|
||||
"DeleteRule": collection.DeleteRule,
|
||||
}
|
||||
|
||||
for ruleKey, rule := range rules {
|
||||
if rule == nil || *rule == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
originalRule := *rule
|
||||
|
||||
for _, replacement := range ruleReplacements {
|
||||
re := regexp.MustCompile(regexp.QuoteMeta(replacement.old) + `\b`)
|
||||
*rule = re.ReplaceAllString(*rule, replacement.new)
|
||||
}
|
||||
|
||||
*rule = replaceReversedLikes(*rule)
|
||||
|
||||
if originalRule != *rule {
|
||||
color.Blue("[%s - replaced %s]:", collection.Name, ruleKey)
|
||||
color.Yellow(" - old: %s", strings.TrimSpace(originalRule))
|
||||
color.Green(" - new: %s", strings.TrimSpace(*rule))
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
// ---
|
||||
|
||||
if err := txDao.SaveCollection(collection); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateUsers(app core.App, txDao *daos.Dao) error {
|
||||
color.Blue(`[merging "_users" and "profiles"]:`)
|
||||
|
||||
profilesCollection, err := txDao.FindCollectionByNameOrId("profiles")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
originalProfilesCollectionId := profilesCollection.Id
|
||||
|
||||
// change the profiles collection id to something else since we will be using
|
||||
// it for the new users collection in order to avoid renaming the storage dir
|
||||
_, idRenameErr := txDao.DB().NewQuery(fmt.Sprintf(
|
||||
`UPDATE {{_collections}}
|
||||
SET id = '%s'
|
||||
WHERE id = '%s';
|
||||
`,
|
||||
(originalProfilesCollectionId + "__old__"),
|
||||
originalProfilesCollectionId,
|
||||
)).Execute()
|
||||
if idRenameErr != nil {
|
||||
return idRenameErr
|
||||
}
|
||||
|
||||
// refresh profiles collection
|
||||
profilesCollection, err = txDao.FindCollectionByNameOrId("profiles")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usersSchema, _ := profilesCollection.Schema.Clone()
|
||||
userIdField := usersSchema.GetFieldByName("userId")
|
||||
if userIdField != nil {
|
||||
usersSchema.RemoveField(userIdField.Id)
|
||||
}
|
||||
|
||||
usersCollection := &models.Collection{}
|
||||
usersCollection.MarkAsNew()
|
||||
usersCollection.Id = originalProfilesCollectionId
|
||||
usersCollection.Name = "users"
|
||||
usersCollection.Type = models.CollectionTypeAuth
|
||||
usersCollection.Schema = *usersSchema
|
||||
usersCollection.CreateRule = types.Pointer("")
|
||||
if profilesCollection.ListRule != nil && *profilesCollection.ListRule != "" {
|
||||
*profilesCollection.ListRule = strings.ReplaceAll(*profilesCollection.ListRule, "userId", "id")
|
||||
usersCollection.ListRule = profilesCollection.ListRule
|
||||
}
|
||||
if profilesCollection.ViewRule != nil && *profilesCollection.ViewRule != "" {
|
||||
*profilesCollection.ViewRule = strings.ReplaceAll(*profilesCollection.ViewRule, "userId", "id")
|
||||
usersCollection.ViewRule = profilesCollection.ViewRule
|
||||
}
|
||||
if profilesCollection.UpdateRule != nil && *profilesCollection.UpdateRule != "" {
|
||||
*profilesCollection.UpdateRule = strings.ReplaceAll(*profilesCollection.UpdateRule, "userId", "id")
|
||||
usersCollection.UpdateRule = profilesCollection.UpdateRule
|
||||
}
|
||||
if profilesCollection.DeleteRule != nil && *profilesCollection.DeleteRule != "" {
|
||||
*profilesCollection.DeleteRule = strings.ReplaceAll(*profilesCollection.DeleteRule, "userId", "id")
|
||||
usersCollection.DeleteRule = profilesCollection.DeleteRule
|
||||
}
|
||||
|
||||
// set auth options
|
||||
settings := app.Settings()
|
||||
authOptions := usersCollection.AuthOptions()
|
||||
authOptions.ManageRule = nil
|
||||
authOptions.AllowOAuth2Auth = true
|
||||
authOptions.AllowUsernameAuth = false
|
||||
authOptions.AllowEmailAuth = settings.EmailAuth.Enabled
|
||||
authOptions.MinPasswordLength = settings.EmailAuth.MinPasswordLength
|
||||
authOptions.OnlyEmailDomains = settings.EmailAuth.OnlyDomains
|
||||
authOptions.ExceptEmailDomains = settings.EmailAuth.ExceptDomains
|
||||
// twitter currently is the only provider that doesn't return an email
|
||||
authOptions.RequireEmail = !settings.TwitterAuth.Enabled
|
||||
|
||||
usersCollection.SetOptions(authOptions)
|
||||
|
||||
if err := txDao.SaveCollection(usersCollection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// copy the original users
|
||||
_, usersErr := txDao.DB().NewQuery(`
|
||||
INSERT INTO {{users}} (id, created, updated, username, email, emailVisibility, verified, tokenKey, passwordHash, lastResetSentAt, lastVerificationSentAt)
|
||||
SELECT id, created, updated, ("u_" || id), email, false, verified, tokenKey, passwordHash, lastResetSentAt, lastVerificationSentAt
|
||||
FROM {{_users}};
|
||||
`).Execute()
|
||||
if usersErr != nil {
|
||||
return usersErr
|
||||
}
|
||||
|
||||
// generate the profile fields copy statements
|
||||
sets := []string{"id = p.id"}
|
||||
for _, f := range usersSchema.Fields() {
|
||||
sets = append(sets, fmt.Sprintf("%s = p.%s", f.Name, f.Name))
|
||||
}
|
||||
|
||||
// copy profile fields
|
||||
_, copyProfileErr := txDao.DB().NewQuery(fmt.Sprintf(`
|
||||
UPDATE {{users}} as u
|
||||
SET %s
|
||||
FROM {{profiles}} as p
|
||||
WHERE u.id = p.userId;
|
||||
`, strings.Join(sets, ", "))).Execute()
|
||||
if copyProfileErr != nil {
|
||||
return copyProfileErr
|
||||
}
|
||||
|
||||
profileRecords, err := txDao.FindRecordsByExpr("profiles")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update all profiles and users fields to point to the new users collection
|
||||
collections := []*models.Collection{}
|
||||
if err := txDao.CollectionQuery().All(&collections); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, collection := range collections {
|
||||
var hasChanges bool
|
||||
|
||||
for _, f := range collection.Schema.Fields() {
|
||||
f.InitOptions()
|
||||
|
||||
if f.Type == schema.FieldTypeUser {
|
||||
if collection.Name == "profiles" && f.Name == "userId" {
|
||||
continue
|
||||
}
|
||||
|
||||
hasChanges = true
|
||||
|
||||
// change the user field to a relation field
|
||||
options, _ := f.Options.(*schema.UserOptions)
|
||||
f.Type = schema.FieldTypeRelation
|
||||
f.Options = &schema.RelationOptions{
|
||||
CollectionId: usersCollection.Id,
|
||||
MaxSelect: &options.MaxSelect,
|
||||
CascadeDelete: options.CascadeDelete,
|
||||
}
|
||||
|
||||
for _, p := range profileRecords {
|
||||
pId := p.Id
|
||||
pUserId := p.GetString("userId")
|
||||
// replace all user record id references with the profile id
|
||||
_, replaceErr := txDao.DB().NewQuery(fmt.Sprintf(`
|
||||
UPDATE %s
|
||||
SET [[%s]] = REPLACE([[%s]], '%s', '%s')
|
||||
WHERE [[%s]] LIKE ('%%%s%%');
|
||||
`, collection.Name, f.Name, f.Name, pUserId, pId, f.Name, pUserId)).Execute()
|
||||
if replaceErr != nil {
|
||||
return replaceErr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasChanges {
|
||||
if err := txDao.Save(collection); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := migrateExternalAuths(txDao, originalProfilesCollectionId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// drop _users table
|
||||
if _, err := txDao.DB().DropTable("_users").Execute(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// drop profiles table
|
||||
if _, err := txDao.DB().DropTable("profiles").Execute(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete profiles collection
|
||||
if err := txDao.Delete(profilesCollection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Green(` - Successfully merged "_users" and "profiles" into a new collection "users".`)
|
||||
fmt.Println()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateExternalAuths(txDao *daos.Dao, userCollectionId string) error {
|
||||
_, alterErr := txDao.DB().NewQuery(`
|
||||
-- crate new externalAuths table
|
||||
CREATE TABLE {{_newExternalAuths}} (
|
||||
[[id]] TEXT PRIMARY KEY,
|
||||
[[collectionId]] TEXT NOT NULL,
|
||||
[[recordId]] TEXT NOT NULL,
|
||||
[[provider]] TEXT NOT NULL,
|
||||
[[providerId]] TEXT NOT NULL,
|
||||
[[created]] TEXT DEFAULT "" NOT NULL,
|
||||
[[updated]] TEXT DEFAULT "" NOT NULL,
|
||||
---
|
||||
FOREIGN KEY ([[collectionId]]) REFERENCES {{_collections}} ([[id]]) ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- copy all data from the old table to the new one
|
||||
INSERT INTO {{_newExternalAuths}}
|
||||
SELECT auth.id, "` + userCollectionId + `" as collectionId, [[profiles.id]] as recordId, auth.provider, auth.providerId, auth.created, auth.updated
|
||||
FROM {{_externalAuths}} auth
|
||||
INNER JOIN {{profiles}} on [[profiles.userId]] = [[auth.userId]];
|
||||
|
||||
-- drop old table
|
||||
DROP TABLE {{_externalAuths}};
|
||||
|
||||
-- rename new table
|
||||
ALTER TABLE {{_newExternalAuths}} RENAME TO {{_externalAuths}};
|
||||
|
||||
-- create named indexes
|
||||
CREATE UNIQUE INDEX _externalAuths_record_provider_idx on {{_externalAuths}} ([[collectionId]], [[recordId]], [[provider]]);
|
||||
CREATE UNIQUE INDEX _externalAuths_provider_providerId_idx on {{_externalAuths}} ([[provider]], [[providerId]]);
|
||||
`).Execute()
|
||||
|
||||
return alterErr
|
||||
}
|
||||
|
||||
func resetMigrationsTable(txDao *daos.Dao) error {
|
||||
// reset the migration state to the new init
|
||||
_, err := txDao.DB().Delete("_migrations", dbx.HashExp{
|
||||
"file": "1661586591_add_externalAuths_table.go",
|
||||
}).Execute()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var reverseLikeRegex = regexp.MustCompile(`(['"]\w*['"])\s*(\~|!~)\s*([\w\@\.]*)`)
|
||||
|
||||
func replaceReversedLikes(rule string) string {
|
||||
parts := reverseLikeRegex.FindAllStringSubmatch(rule, -1)
|
||||
|
||||
for _, p := range parts {
|
||||
if len(p) != 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
newPart := fmt.Sprintf("%s %s %s", p[3], p[2], p[1])
|
||||
|
||||
rule = strings.ReplaceAll(rule, p[0], newPart)
|
||||
}
|
||||
|
||||
return rule
|
||||
}
|
||||
+40
-1
@@ -4,6 +4,8 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models/settings"
|
||||
@@ -69,12 +71,20 @@ type App interface {
|
||||
// NewMailClient creates and returns a configured app mail client.
|
||||
NewMailClient() mailer.Mailer
|
||||
|
||||
// NewFilesystem creates and returns a configured filesystem.System instance.
|
||||
// NewFilesystem creates and returns a configured filesystem.System instance
|
||||
// for managing regular app files (eg. collection uploads).
|
||||
//
|
||||
// NB! Make sure to call `Close()` on the returned result
|
||||
// after you are done working with it.
|
||||
NewFilesystem() (*filesystem.System, error)
|
||||
|
||||
// NewBackupsFilesystem creates and returns a configured filesystem.System instance
|
||||
// for managing app backups.
|
||||
//
|
||||
// NB! Make sure to call `Close()` on the returned result
|
||||
// after you are done working with it.
|
||||
NewBackupsFilesystem() (*filesystem.System, error)
|
||||
|
||||
// RefreshSettings reinitializes and reloads the stored application settings.
|
||||
RefreshSettings() error
|
||||
|
||||
@@ -92,6 +102,31 @@ type App interface {
|
||||
// (eg. closing db connections).
|
||||
ResetBootstrapState() error
|
||||
|
||||
// CreateBackup creates a new backup of the current app pb_data directory.
|
||||
//
|
||||
// Backups can be stored on S3 if it is configured in app.Settings().Backups.
|
||||
//
|
||||
// Please refer to the godoc of the specific core.App implementation
|
||||
// for details on the backup procedures.
|
||||
CreateBackup(ctx context.Context, name string) error
|
||||
|
||||
// RestoreBackup restores the backup with the specified name and restarts
|
||||
// the current running application process.
|
||||
//
|
||||
// The safely perform the restore it is recommended to have free disk space
|
||||
// for at least 2x the size of the restored pb_data backup.
|
||||
//
|
||||
// Please refer to the godoc of the specific core.App implementation
|
||||
// for details on the restore procedures.
|
||||
//
|
||||
// NB! This feature is experimental and currently is expected to work only on UNIX based systems.
|
||||
RestoreBackup(ctx context.Context, name string) error
|
||||
|
||||
// Restart restarts the current running application process.
|
||||
//
|
||||
// Currently it is relying on execve so it is supported only on UNIX based systems.
|
||||
Restart() error
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// App event hooks
|
||||
// ---------------------------------------------------------------
|
||||
@@ -118,6 +153,10 @@ type App interface {
|
||||
// It could be used to log the final API error in external services.
|
||||
OnAfterApiError() *hook.Hook[*ApiErrorEvent]
|
||||
|
||||
// OnTerminate hook is triggered when the app is in the process
|
||||
// of being terminated (eg. on SIGTERM signal).
|
||||
OnTerminate() *hook.Hook[*TerminateEvent]
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Dao event hooks
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
+73
-1
@@ -7,6 +7,8 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
@@ -27,6 +29,10 @@ const (
|
||||
DefaultDataMaxIdleConns int = 20
|
||||
DefaultLogsMaxOpenConns int = 10
|
||||
DefaultLogsMaxIdleConns int = 2
|
||||
|
||||
LocalStorageDirName string = "storage"
|
||||
LocalBackupsDirName string = "backups"
|
||||
LocalTempDirName string = ".pb_temp_to_delete" // temp pb_data sub directory that will be deleted on each app.Bootstrap()
|
||||
)
|
||||
|
||||
var _ App = (*BaseApp)(nil)
|
||||
@@ -55,6 +61,7 @@ type BaseApp struct {
|
||||
onBeforeServe *hook.Hook[*ServeEvent]
|
||||
onBeforeApiError *hook.Hook[*ApiErrorEvent]
|
||||
onAfterApiError *hook.Hook[*ApiErrorEvent]
|
||||
onTerminate *hook.Hook[*TerminateEvent]
|
||||
|
||||
// dao event hooks
|
||||
onModelBeforeCreate *hook.Hook[*ModelEvent]
|
||||
@@ -192,6 +199,7 @@ func NewBaseApp(config *BaseAppConfig) *BaseApp {
|
||||
onBeforeServe: &hook.Hook[*ServeEvent]{},
|
||||
onBeforeApiError: &hook.Hook[*ApiErrorEvent]{},
|
||||
onAfterApiError: &hook.Hook[*ApiErrorEvent]{},
|
||||
onTerminate: &hook.Hook[*TerminateEvent]{},
|
||||
|
||||
// dao event hooks
|
||||
onModelBeforeCreate: &hook.Hook[*ModelEvent]{},
|
||||
@@ -338,6 +346,9 @@ func (app *BaseApp) Bootstrap() error {
|
||||
// we don't check for an error because the db migrations may have not been executed yet
|
||||
app.RefreshSettings()
|
||||
|
||||
// cleanup the pb_data temp directory (if any)
|
||||
os.RemoveAll(filepath.Join(app.DataDir(), LocalTempDirName))
|
||||
|
||||
if err := app.OnAfterBootstrap().Trigger(event); err != nil && app.IsDebug() {
|
||||
log.Println(err)
|
||||
}
|
||||
@@ -471,6 +482,7 @@ func (app *BaseApp) NewMailClient() mailer.Mailer {
|
||||
}
|
||||
|
||||
// NewFilesystem creates a new local or S3 filesystem instance
|
||||
// for managing regular app files (eg. collection uploads)
|
||||
// based on the current app settings.
|
||||
//
|
||||
// NB! Make sure to call `Close()` on the returned result
|
||||
@@ -488,7 +500,54 @@ func (app *BaseApp) NewFilesystem() (*filesystem.System, error) {
|
||||
}
|
||||
|
||||
// fallback to local filesystem
|
||||
return filesystem.NewLocal(filepath.Join(app.DataDir(), "storage"))
|
||||
return filesystem.NewLocal(filepath.Join(app.DataDir(), LocalStorageDirName))
|
||||
}
|
||||
|
||||
// NewFilesystem creates a new local or S3 filesystem instance
|
||||
// for managing app backups based on the current app settings.
|
||||
//
|
||||
// NB! Make sure to call `Close()` on the returned result
|
||||
// after you are done working with it.
|
||||
func (app *BaseApp) NewBackupsFilesystem() (*filesystem.System, error) {
|
||||
if app.settings != nil && app.settings.Backups.S3.Enabled {
|
||||
return filesystem.NewS3(
|
||||
app.settings.Backups.S3.Bucket,
|
||||
app.settings.Backups.S3.Region,
|
||||
app.settings.Backups.S3.Endpoint,
|
||||
app.settings.Backups.S3.AccessKey,
|
||||
app.settings.Backups.S3.Secret,
|
||||
app.settings.Backups.S3.ForcePathStyle,
|
||||
)
|
||||
}
|
||||
|
||||
// fallback to local filesystem
|
||||
return filesystem.NewLocal(filepath.Join(app.DataDir(), LocalBackupsDirName))
|
||||
}
|
||||
|
||||
// Restart restarts (aka. replaces) the current running application process.
|
||||
//
|
||||
// NB! It relies on execve which is supported only on UNIX based systems.
|
||||
func (app *BaseApp) Restart() error {
|
||||
if runtime.GOOS == "windows" {
|
||||
return errors.New("restart is not supported on windows")
|
||||
}
|
||||
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// optimistically reset the app bootstrap state
|
||||
app.ResetBootstrapState()
|
||||
|
||||
if err := syscall.Exec(execPath, os.Args, os.Environ()); err != nil {
|
||||
// restart the app bootstrap state
|
||||
app.Bootstrap()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshSettings reinitializes and reloads the stored application settings.
|
||||
@@ -541,6 +600,10 @@ func (app *BaseApp) OnAfterApiError() *hook.Hook[*ApiErrorEvent] {
|
||||
return app.onAfterApiError
|
||||
}
|
||||
|
||||
func (app *BaseApp) OnTerminate() *hook.Hook[*TerminateEvent] {
|
||||
return app.onTerminate
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Dao event hooks
|
||||
// -------------------------------------------------------------------
|
||||
@@ -1084,4 +1147,13 @@ func (app *BaseApp) registerDefaultHooks() {
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
app.OnTerminate().Add(func(e *TerminateEvent) error {
|
||||
app.ResetBootstrapState()
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := app.initAutobackupHooks(); err != nil && app.IsDebug() {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/tools/archive"
|
||||
"github.com/pocketbase/pocketbase/tools/cron"
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||
"github.com/pocketbase/pocketbase/tools/osutils"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
const CacheKeyActiveBackup string = "@activeBackup"
|
||||
|
||||
// CreateBackup creates a new backup of the current app pb_data directory.
|
||||
//
|
||||
// If name is empty, it will be autogenerated.
|
||||
// If backup with the same name exists, the new backup file will replace it.
|
||||
//
|
||||
// The backup is executed within a transaction, meaning that new writes
|
||||
// will be temporary "blocked" until the backup file is generated.
|
||||
//
|
||||
// By default backups are stored in pb_data/backups
|
||||
// (the backups directory itself is excluded from the generated backup).
|
||||
//
|
||||
// When using S3 storage for the uploaded collection files, you have to
|
||||
// take care manually to backup those since they are not part of the pb_data.
|
||||
//
|
||||
// Backups can be stored on S3 if it is configured in app.Settings().Backups.
|
||||
func (app *BaseApp) CreateBackup(ctx context.Context, name string) error {
|
||||
if app.Cache().Has(CacheKeyActiveBackup) {
|
||||
return errors.New("try again later - another backup/restore operation has already been started")
|
||||
}
|
||||
|
||||
// auto generate backup name
|
||||
if name == "" {
|
||||
name = fmt.Sprintf(
|
||||
"pb_backup_%s.zip",
|
||||
time.Now().UTC().Format("20060102150405"),
|
||||
)
|
||||
}
|
||||
|
||||
app.Cache().Set(CacheKeyActiveBackup, name)
|
||||
defer app.Cache().Remove(CacheKeyActiveBackup)
|
||||
|
||||
// Archive pb_data in a temp directory, exluding the "backups" dir itself (if exist).
|
||||
//
|
||||
// Run in transaction to temporary block other writes (transactions uses the NonconcurrentDB connection).
|
||||
// ---
|
||||
tempPath := filepath.Join(os.TempDir(), "pb_backup_"+security.PseudorandomString(4))
|
||||
createErr := app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
|
||||
if err := archive.Create(app.DataDir(), tempPath, LocalBackupsDirName); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if createErr != nil {
|
||||
return createErr
|
||||
}
|
||||
defer os.Remove(tempPath)
|
||||
|
||||
// Persist the backup in the backups filesystem.
|
||||
// ---
|
||||
fsys, err := app.NewBackupsFilesystem()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fsys.Close()
|
||||
|
||||
fsys.SetContext(ctx)
|
||||
|
||||
file, err := filesystem.NewFileFromPath(tempPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file.OriginalName = name
|
||||
file.Name = file.OriginalName
|
||||
|
||||
if err := fsys.UploadFile(file, file.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestoreBackup restores the backup with the specified name and restarts
|
||||
// the current running application process.
|
||||
//
|
||||
// NB! This feature is experimental and currently is expected to work only on UNIX based systems.
|
||||
//
|
||||
// To safely perform the restore it is recommended to have free disk space
|
||||
// for at least 2x the size of the restored pb_data backup.
|
||||
//
|
||||
// The performed steps are:
|
||||
//
|
||||
// 1. Download the backup with the specified name in a temp location
|
||||
// (this is in case of S3; otherwise it creates a temp copy of the zip)
|
||||
//
|
||||
// 2. Extract the backup in a temp directory inside the app "pb_data"
|
||||
// (eg. "pb_data/.pb_temp_to_delete/pb_restore").
|
||||
//
|
||||
// 3. Move the current app "pb_data" content (excluding the local backups and the special temp dir)
|
||||
// under another temp sub dir that will be deleted on the next app start up
|
||||
// (eg. "pb_data/.pb_temp_to_delete/old_pb_data").
|
||||
// This is because on some environments it may not be allowed
|
||||
// to delete the currently open "pb_data" files.
|
||||
//
|
||||
// 4. Move the extracted dir content to the app "pb_data".
|
||||
//
|
||||
// 5. Restart the app (on successfull app bootstap it will also remove the old pb_data).
|
||||
//
|
||||
// If a failure occure during the restore process the dir changes are reverted.
|
||||
// If for whatever reason the revert is not possible, it panics.
|
||||
func (app *BaseApp) RestoreBackup(ctx context.Context, name string) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
return errors.New("restore is not supported on windows")
|
||||
}
|
||||
|
||||
if app.Cache().Has(CacheKeyActiveBackup) {
|
||||
return errors.New("try again later - another backup/restore operation has already been started")
|
||||
}
|
||||
|
||||
app.Cache().Set(CacheKeyActiveBackup, name)
|
||||
defer app.Cache().Remove(CacheKeyActiveBackup)
|
||||
|
||||
fsys, err := app.NewBackupsFilesystem()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fsys.Close()
|
||||
|
||||
fsys.SetContext(ctx)
|
||||
|
||||
// fetch the backup file in a temp location
|
||||
br, err := fsys.GetFile(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer br.Close()
|
||||
|
||||
tempZip, err := os.CreateTemp(os.TempDir(), "pb_restore")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tempZip.Name())
|
||||
|
||||
if _, err := io.Copy(tempZip, br); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// make sure that the special temp directory
|
||||
if err := os.MkdirAll(filepath.Join(app.DataDir(), LocalTempDirName), os.ModePerm); err != nil {
|
||||
return fmt.Errorf("failed to create a temp dir: %w", err)
|
||||
}
|
||||
|
||||
// note: it needs to be inside the current pb_data to avoid "cross-device link" errors
|
||||
extractedDataDir := filepath.Join(app.DataDir(), LocalTempDirName, "pb_restore_"+security.PseudorandomString(4))
|
||||
defer os.RemoveAll(extractedDataDir)
|
||||
if err := archive.Extract(tempZip.Name(), extractedDataDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ensure that a database file exists
|
||||
extractedDB := filepath.Join(extractedDataDir, "data.db")
|
||||
if _, err := os.Stat(extractedDB); err != nil {
|
||||
return fmt.Errorf("data.db file is missing or invalid: %w", err)
|
||||
}
|
||||
|
||||
// remove the extracted zip file since we no longer need it
|
||||
// (this is in case the app restarts and the defer calls are not called)
|
||||
if err := os.Remove(tempZip.Name()); err != nil && app.IsDebug() {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
// root dir entries to exclude from the backup restore
|
||||
exclude := []string{LocalBackupsDirName, LocalTempDirName}
|
||||
|
||||
// move the current pb_data content to a special temp location
|
||||
// that will hold the old data between dirs replace
|
||||
// (the temp dir will be automatically removed on the next app start)
|
||||
oldTempDataDir := filepath.Join(app.DataDir(), LocalTempDirName, "old_pb_data_"+security.PseudorandomString(4))
|
||||
if err := osutils.MoveDirContent(app.DataDir(), oldTempDataDir, exclude...); err != nil {
|
||||
return fmt.Errorf("failed to move the current pb_data content to a temp location: %w", err)
|
||||
}
|
||||
|
||||
// move the extracted archive content to the app's pb_data
|
||||
if err := osutils.MoveDirContent(extractedDataDir, app.DataDir(), exclude...); err != nil {
|
||||
return fmt.Errorf("failed to move the extracted archive content to pb_data: %w", err)
|
||||
}
|
||||
|
||||
revertDataDirChanges := func() error {
|
||||
if err := osutils.MoveDirContent(app.DataDir(), extractedDataDir, exclude...); err != nil {
|
||||
return fmt.Errorf("failed to revert the extracted dir change: %w", err)
|
||||
}
|
||||
|
||||
if err := osutils.MoveDirContent(oldTempDataDir, app.DataDir(), exclude...); err != nil {
|
||||
return fmt.Errorf("failed to revert old pb_data dir change: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// restart the app
|
||||
if err := app.Restart(); err != nil {
|
||||
if err := revertDataDirChanges(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to restart the app process: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initAutobackupHooks registers the autobackup app serve hooks.
|
||||
// @todo add tests
|
||||
func (app *BaseApp) initAutobackupHooks() error {
|
||||
c := cron.New()
|
||||
isServe := false
|
||||
|
||||
loadJob := func() {
|
||||
c.Stop()
|
||||
|
||||
rawSchedule := app.Settings().Backups.Cron
|
||||
if rawSchedule == "" || !isServe || !app.IsBootstrapped() {
|
||||
return
|
||||
}
|
||||
|
||||
c.Add("@autobackup", rawSchedule, func() {
|
||||
autoPrefix := "@auto_pb_backup_"
|
||||
|
||||
name := fmt.Sprintf(
|
||||
"%s%s.zip",
|
||||
autoPrefix,
|
||||
time.Now().UTC().Format("20060102150405"),
|
||||
)
|
||||
|
||||
if err := app.CreateBackup(context.Background(), name); err != nil && app.IsDebug() {
|
||||
// @todo replace after logs generalization
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
maxKeep := app.Settings().Backups.CronMaxKeep
|
||||
|
||||
if maxKeep == 0 {
|
||||
return // no explicit limit
|
||||
}
|
||||
|
||||
fsys, err := app.NewBackupsFilesystem()
|
||||
if err != nil && app.IsDebug() {
|
||||
// @todo replace after logs generalization
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
defer fsys.Close()
|
||||
|
||||
files, err := fsys.List(autoPrefix)
|
||||
if err != nil && app.IsDebug() {
|
||||
// @todo replace after logs generalization
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
if maxKeep >= len(files) {
|
||||
return // nothing to remove
|
||||
}
|
||||
|
||||
// sort desc
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return files[i].ModTime.After(files[j].ModTime)
|
||||
})
|
||||
|
||||
// keep only the most recent n auto backup files
|
||||
toRemove := files[maxKeep:]
|
||||
|
||||
for _, f := range toRemove {
|
||||
if err := fsys.Delete(f.Key); err != nil && app.IsDebug() {
|
||||
// @todo replace after logs generalization
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// restart the ticker
|
||||
c.Start()
|
||||
}
|
||||
|
||||
// load on app serve
|
||||
app.OnBeforeServe().Add(func(e *ServeEvent) error {
|
||||
isServe = true
|
||||
loadJob()
|
||||
return nil
|
||||
})
|
||||
|
||||
// stop the ticker on app termination
|
||||
app.OnTerminate().Add(func(e *TerminateEvent) error {
|
||||
c.Stop()
|
||||
return nil
|
||||
})
|
||||
|
||||
// reload on app settings change
|
||||
app.OnModelAfterUpdate((&models.Param{}).TableName()).Add(func(e *ModelEvent) error {
|
||||
p := e.Model.(*models.Param)
|
||||
if p == nil || p.Key != models.ParamAppSettings {
|
||||
return nil
|
||||
}
|
||||
|
||||
loadJob()
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package core_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"github.com/pocketbase/pocketbase/tools/archive"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
)
|
||||
|
||||
func TestCreateBackup(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
// test pending error
|
||||
app.Cache().Set(core.CacheKeyActiveBackup, "")
|
||||
if err := app.CreateBackup(context.Background(), "test.zip"); err == nil {
|
||||
t.Fatal("Expected pending error, got nil")
|
||||
}
|
||||
app.Cache().Remove(core.CacheKeyActiveBackup)
|
||||
|
||||
// create with auto generated name
|
||||
if err := app.CreateBackup(context.Background(), ""); err != nil {
|
||||
t.Fatal("Failed to create a backup with autogenerated name")
|
||||
}
|
||||
|
||||
// create with custom name
|
||||
if err := app.CreateBackup(context.Background(), "custom"); err != nil {
|
||||
t.Fatal("Failed to create a backup with custom name")
|
||||
}
|
||||
|
||||
// create new with the same name (aka. replace)
|
||||
if err := app.CreateBackup(context.Background(), "custom"); err != nil {
|
||||
t.Fatal("Failed to create and replace a backup with the same name")
|
||||
}
|
||||
|
||||
backupsDir := filepath.Join(app.DataDir(), core.LocalBackupsDirName)
|
||||
|
||||
entries, err := os.ReadDir(backupsDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedFiles := []string{
|
||||
`^pb_backup_\w+\.zip$`,
|
||||
`^pb_backup_\w+\.zip.attrs$`,
|
||||
"custom",
|
||||
"custom.attrs",
|
||||
}
|
||||
|
||||
if len(entries) != len(expectedFiles) {
|
||||
names := getEntryNames(entries)
|
||||
t.Fatalf("Expected %d backup files, got %d: \n%v", len(expectedFiles), len(entries), names)
|
||||
}
|
||||
|
||||
for i, entry := range entries {
|
||||
if !list.ExistInSliceWithRegex(entry.Name(), expectedFiles) {
|
||||
t.Fatalf("[%d] Missing backup file %q", i, entry.Name())
|
||||
}
|
||||
|
||||
if strings.HasSuffix(entry.Name(), ".attrs") {
|
||||
continue
|
||||
}
|
||||
|
||||
path := filepath.Join(backupsDir, entry.Name())
|
||||
|
||||
if err := verifyBackupContent(app, path); err != nil {
|
||||
t.Fatalf("[%d] Failed to verify backup content: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreBackup(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
// create a initial test backup to ensure that there are at least 1
|
||||
// backup file and that the generated zip doesn't contain the backups dir
|
||||
if err := app.CreateBackup(context.Background(), "initial"); err != nil {
|
||||
t.Fatal("Failed to create test initial backup")
|
||||
}
|
||||
|
||||
// create test backup
|
||||
if err := app.CreateBackup(context.Background(), "test"); err != nil {
|
||||
t.Fatal("Failed to create test backup")
|
||||
}
|
||||
|
||||
// test pending error
|
||||
app.Cache().Set(core.CacheKeyActiveBackup, "")
|
||||
if err := app.RestoreBackup(context.Background(), "test"); err == nil {
|
||||
t.Fatal("Expected pending error, got nil")
|
||||
}
|
||||
app.Cache().Remove(core.CacheKeyActiveBackup)
|
||||
|
||||
// missing backup
|
||||
if err := app.RestoreBackup(context.Background(), "missing"); err == nil {
|
||||
t.Fatal("Expected missing error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
func verifyBackupContent(app core.App, path string) error {
|
||||
dir, err := os.MkdirTemp("", "backup_test")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
if err := archive.Extract(path, dir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expectedRootEntries := []string{
|
||||
"storage",
|
||||
"data.db",
|
||||
"data.db-shm",
|
||||
"data.db-wal",
|
||||
"logs.db",
|
||||
"logs.db-shm",
|
||||
"logs.db-wal",
|
||||
".gitignore",
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(entries) != len(expectedRootEntries) {
|
||||
names := getEntryNames(entries)
|
||||
return fmt.Errorf("Expected %d backup files, got %d: \n%v", len(expectedRootEntries), len(entries), names)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !list.ExistInSliceWithRegex(entry.Name(), expectedRootEntries) {
|
||||
return fmt.Errorf("Didn't expect %q entry", entry.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getEntryNames(entries []fs.DirEntry) []string {
|
||||
names := make([]string, len(entries))
|
||||
|
||||
for i, entry := range entries {
|
||||
names[i] = entry.Name()
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
@@ -236,3 +236,33 @@ func TestBaseAppNewFilesystem(t *testing.T) {
|
||||
t.Fatalf("Expected nil s3 filesystem, got %v", s3)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseAppNewBackupsFilesystem(t *testing.T) {
|
||||
const testDataDir = "./pb_base_app_test_data_dir/"
|
||||
defer os.RemoveAll(testDataDir)
|
||||
|
||||
app := NewBaseApp(&BaseAppConfig{
|
||||
DataDir: testDataDir,
|
||||
EncryptionEnv: "pb_test_env",
|
||||
IsDebug: false,
|
||||
})
|
||||
|
||||
// local
|
||||
local, localErr := app.NewBackupsFilesystem()
|
||||
if localErr != nil {
|
||||
t.Fatal(localErr)
|
||||
}
|
||||
if local == nil {
|
||||
t.Fatal("Expected local backups filesystem instance, got nil")
|
||||
}
|
||||
|
||||
// misconfigured s3
|
||||
app.Settings().Backups.S3.Enabled = true
|
||||
s3, s3Err := app.NewBackupsFilesystem()
|
||||
if s3Err == nil {
|
||||
t.Fatal("Expected S3 error, got nil")
|
||||
}
|
||||
if s3 != nil {
|
||||
t.Fatalf("Expected nil s3 backups filesystem, got %v", s3)
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
||||
func initPragmas(db *dbx.DB) error {
|
||||
// note: the busy_timeout pragma must be first because
|
||||
// the connection needs to be set to block on busy before WAL mode
|
||||
// is set in case it hasn't been already set by another connection
|
||||
_, err := db.NewQuery(`
|
||||
PRAGMA busy_timeout = 10000;
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA journal_size_limit = 200000000;
|
||||
PRAGMA synchronous = NORMAL;
|
||||
PRAGMA foreign_keys = TRUE;
|
||||
`).Execute()
|
||||
|
||||
return err
|
||||
}
|
||||
+34
-8
@@ -3,18 +3,44 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/mattn/go-sqlite3"
|
||||
"github.com/pocketbase/dbx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func connectDB(dbPath string) (*dbx.DB, error) {
|
||||
db, err := dbx.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func init() {
|
||||
// Registers the sqlite3 driver with a ConnectHook so that we can
|
||||
// initialize the default PRAGMAs.
|
||||
//
|
||||
// Note 1: we don't define the PRAGMA as part of the dsn string
|
||||
// because not all pragmas are available.
|
||||
//
|
||||
// Note 2: the busy_timeout pragma must be first because
|
||||
// the connection needs to be set to block on busy before WAL mode
|
||||
// is set in case it hasn't been already set by another connection.
|
||||
sql.Register("pb_sqlite3",
|
||||
&sqlite3.SQLiteDriver{
|
||||
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
|
||||
_, err := conn.Exec(`
|
||||
PRAGMA busy_timeout = 10000;
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA journal_size_limit = 200000000;
|
||||
PRAGMA synchronous = NORMAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
`, nil)
|
||||
|
||||
if err := initPragmas(db); err != nil {
|
||||
db.Close()
|
||||
return err
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
dbx.BuilderFuncMap["pb_sqlite3"] = dbx.BuilderFuncMap["sqlite3"]
|
||||
}
|
||||
|
||||
func connectDB(dbPath string) (*dbx.DB, error) {
|
||||
db, err := dbx.Open("pb_sqlite3", dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -8,13 +8,13 @@ import (
|
||||
)
|
||||
|
||||
func connectDB(dbPath string) (*dbx.DB, error) {
|
||||
db, err := dbx.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Note: the busy_timeout pragma must be first because
|
||||
// the connection needs to be set to block on busy before WAL mode
|
||||
// is set in case it hasn't been already set by another connection.
|
||||
pragmas := "?_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)&_pragma=journal_size_limit(200000000)&_pragma=synchronous(NORMAL)&_pragma=foreign_keys(ON)"
|
||||
|
||||
if err := initPragmas(db); err != nil {
|
||||
db.Close()
|
||||
db, err := dbx.Open("sqlite", dbPath+pragmas)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,10 @@ type BootstrapEvent struct {
|
||||
App App
|
||||
}
|
||||
|
||||
type TerminateEvent struct {
|
||||
App App
|
||||
}
|
||||
|
||||
type ServeEvent struct {
|
||||
App App
|
||||
Router *echo.Echo
|
||||
|
||||
+25
-4
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
@@ -166,9 +167,9 @@ func TestDeleteCollection(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
colEmpty := &models.Collection{}
|
||||
colUnsaved := &models.Collection{}
|
||||
|
||||
colAuth, err := app.Dao().FindCollectionByNameOrId("clients")
|
||||
colAuth, err := app.Dao().FindCollectionByNameOrId("users")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -187,6 +188,11 @@ func TestDeleteCollection(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
colBase, err := app.Dao().FindCollectionByNameOrId("demo1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
colView1, err := app.Dao().FindCollectionByNameOrId("view1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -201,12 +207,14 @@ func TestDeleteCollection(t *testing.T) {
|
||||
model *models.Collection
|
||||
expectError bool
|
||||
}{
|
||||
{colEmpty, true},
|
||||
{colAuth, false},
|
||||
{colUnsaved, true},
|
||||
{colReferenced, true},
|
||||
{colSystem, true},
|
||||
{colView1, true}, // view2 depend on it
|
||||
{colView2, false},
|
||||
{colView1, false}, // no longer has dependent collections
|
||||
{colBase, false},
|
||||
{colAuth, false}, // should delete also its related external auths
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
@@ -225,6 +233,19 @@ func TestDeleteCollection(t *testing.T) {
|
||||
if app.Dao().HasTable(s.model.Name) {
|
||||
t.Errorf("[%d] Expected table/view %s to be deleted", i, s.model.Name)
|
||||
}
|
||||
|
||||
// check if the external auths were deleted
|
||||
if s.model.IsAuth() {
|
||||
var total int
|
||||
err := app.Dao().ExternalAuthQuery().
|
||||
Select("count(*)").
|
||||
AndWhere(dbx.HashExp{"collectionId": s.model.Id}).
|
||||
Row(&total)
|
||||
|
||||
if err != nil || total > 0 {
|
||||
t.Fatalf("[%d] Expected external auths to be deleted, got %v (%v)", i, total, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -127,7 +127,7 @@ func (dao *Dao) CreateViewSchema(selectQuery string) (schema.Schema, error) {
|
||||
}
|
||||
|
||||
if !hasId {
|
||||
return errors.New("missing required id column (you ca use `(ROW_NUMBER() OVER()) as id` if you don't have one)")
|
||||
return errors.New("missing required id column (you can use `(ROW_NUMBER() OVER()) as id` if you don't have one)")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/plugins/ghupdate"
|
||||
"github.com/pocketbase/pocketbase/plugins/jsvm"
|
||||
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
||||
)
|
||||
@@ -79,6 +80,9 @@ func main() {
|
||||
Dir: migrationsDir,
|
||||
})
|
||||
|
||||
// GitHub selfupdate
|
||||
ghupdate.MustRegister(app, app.RootCmd, nil)
|
||||
|
||||
app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error {
|
||||
app.Dao().ModelQueryTimeout = time.Duration(queryTimeout) * time.Second
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package forms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
var backupNameRegex = regexp.MustCompile(`^[a-z0-9_-]+\.zip$`)
|
||||
|
||||
// BackupCreate is a request form for creating a new app backup.
|
||||
type BackupCreate struct {
|
||||
app core.App
|
||||
ctx context.Context
|
||||
|
||||
Name string `form:"name" json:"name"`
|
||||
}
|
||||
|
||||
// NewBackupCreate creates new BackupCreate request form.
|
||||
func NewBackupCreate(app core.App) *BackupCreate {
|
||||
return &BackupCreate{
|
||||
app: app,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
}
|
||||
|
||||
// SetContext replaces the default form context with the provided one.
|
||||
func (form *BackupCreate) SetContext(ctx context.Context) {
|
||||
form.ctx = ctx
|
||||
}
|
||||
|
||||
// Validate makes the form validatable by implementing [validation.Validatable] interface.
|
||||
func (form *BackupCreate) Validate() error {
|
||||
return validation.ValidateStruct(form,
|
||||
validation.Field(
|
||||
&form.Name,
|
||||
validation.Length(1, 100),
|
||||
validation.Match(backupNameRegex),
|
||||
validation.By(form.checkUniqueName),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (form *BackupCreate) checkUniqueName(value any) error {
|
||||
v, _ := value.(string)
|
||||
if v == "" {
|
||||
return nil // nothing to check
|
||||
}
|
||||
|
||||
fsys, err := form.app.NewBackupsFilesystem()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fsys.Close()
|
||||
|
||||
fsys.SetContext(form.ctx)
|
||||
|
||||
if exists, err := fsys.Exists(v); err != nil || exists {
|
||||
return validation.NewError("validation_backup_name_exists", "The backup file name is invalid or already exists.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Submit validates the form and creates the app backup.
|
||||
//
|
||||
// You can optionally provide a list of InterceptorFunc to further
|
||||
// modify the form behavior before creating the backup.
|
||||
func (form *BackupCreate) Submit(interceptors ...InterceptorFunc[string]) error {
|
||||
if err := form.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runInterceptors(form.Name, func(name string) error {
|
||||
return form.app.CreateBackup(form.ctx, name)
|
||||
}, interceptors...)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package forms_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/pocketbase/forms"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
)
|
||||
|
||||
func TestBackupCreateValidateAndSubmit(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
backupName string
|
||||
expectedErrors []string
|
||||
}{
|
||||
{
|
||||
"invalid length",
|
||||
strings.Repeat("a", 97) + ".zip",
|
||||
[]string{"name"},
|
||||
},
|
||||
{
|
||||
"valid length + invalid format",
|
||||
strings.Repeat("a", 96),
|
||||
[]string{"name"},
|
||||
},
|
||||
{
|
||||
"valid length + valid format",
|
||||
strings.Repeat("a", 96) + ".zip",
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"auto generated name",
|
||||
"",
|
||||
[]string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
func() {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
fsys, err := app.NewBackupsFilesystem()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer fsys.Close()
|
||||
|
||||
form := forms.NewBackupCreate(app)
|
||||
form.Name = s.backupName
|
||||
|
||||
result := form.Submit()
|
||||
|
||||
// parse errors
|
||||
errs, ok := result.(validation.Errors)
|
||||
if !ok && result != nil {
|
||||
t.Errorf("[%s] Failed to parse errors %v", s.name, result)
|
||||
return
|
||||
}
|
||||
|
||||
// check errors
|
||||
if len(errs) > len(s.expectedErrors) {
|
||||
t.Errorf("[%s] Expected error keys %v, got %v", s.name, s.expectedErrors, errs)
|
||||
}
|
||||
for _, k := range s.expectedErrors {
|
||||
if _, ok := errs[k]; !ok {
|
||||
t.Errorf("[%s] Missing expected error key %q in %v", s.name, k, errs)
|
||||
}
|
||||
}
|
||||
|
||||
// retrieve all created backup files
|
||||
files, err := fsys.List("")
|
||||
if err != nil {
|
||||
t.Errorf("[%s] Failed to retrieve backup files", s.name)
|
||||
return
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
if total := len(files); total != 0 {
|
||||
t.Errorf("[%s] Didn't expected backup files, found %d", s.name, total)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if total := len(files); total != 1 {
|
||||
t.Errorf("[%s] Expected 1 backup file, got %d", s.name, total)
|
||||
return
|
||||
}
|
||||
|
||||
if s.backupName == "" {
|
||||
prefix := "pb_backup_"
|
||||
if !strings.HasPrefix(files[0].Key, prefix) {
|
||||
t.Errorf("[%s] Expected the backup file, to have prefix %q: %q", s.name, prefix, files[0].Key)
|
||||
}
|
||||
} else if s.backupName != files[0].Key {
|
||||
t.Errorf("[%s] Expected backup file %q, got %q", s.name, s.backupName, files[0].Key)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -896,7 +896,7 @@ func (form *RecordUpsert) prepareError(err error) error {
|
||||
c := form.record.Collection()
|
||||
for _, f := range c.Schema.Fields() {
|
||||
// blank space to unify multi-columns lookup
|
||||
if strings.Contains(msg+" ", fmt.Sprintf("%s.%s ", strings.ToLower(c.Name), f.Name)) {
|
||||
if strings.Contains(msg+" ", strings.ToLower(c.Name+"."+f.Name)) {
|
||||
validationErrs[f.Name] = validation.NewError("validation_not_unique", "Value must be unique")
|
||||
}
|
||||
}
|
||||
|
||||
+119
-8
@@ -12,14 +12,17 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/forms"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
func hasRecordFile(app core.App, record *models.Record, filename string) bool {
|
||||
@@ -712,8 +715,11 @@ func TestRecordUpsertWithCustomId(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRecordUpsertAuthRecord(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
name string
|
||||
existingId string
|
||||
data map[string]any
|
||||
manageAccess bool
|
||||
@@ -907,9 +913,6 @@ func TestRecordUpsertAuthRecord(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
collection, err := app.Dao().FindCollectionByNameOrId("users")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -920,7 +923,7 @@ func TestRecordUpsertAuthRecord(t *testing.T) {
|
||||
var err error
|
||||
record, err = app.Dao().FindRecordById(collection.Id, s.existingId)
|
||||
if err != nil {
|
||||
t.Errorf("[%s] Failed to fetch auth record with id %s", s.testName, s.existingId)
|
||||
t.Errorf("[%s] Failed to fetch auth record with id %s", s.name, s.existingId)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -928,7 +931,7 @@ func TestRecordUpsertAuthRecord(t *testing.T) {
|
||||
form := forms.NewRecordUpsert(app, record)
|
||||
form.SetFullManageAccess(s.manageAccess)
|
||||
if err := form.LoadData(s.data); err != nil {
|
||||
t.Errorf("[%s] Failed to load form data", s.testName)
|
||||
t.Errorf("[%s] Failed to load form data", s.name)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -936,11 +939,119 @@ func TestRecordUpsertAuthRecord(t *testing.T) {
|
||||
|
||||
hasErr := submitErr != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.testName, s.expectError, hasErr, submitErr)
|
||||
t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, submitErr)
|
||||
}
|
||||
|
||||
if !hasErr && record.Username() == "" {
|
||||
t.Errorf("[%s] Expected username to be set, got empty string: \n%v", s.testName, record)
|
||||
t.Errorf("[%s] Expected username to be set, got empty string: \n%v", s.name, record)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordUpsertUniqueValidator(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
// create a dummy collection
|
||||
collection := &models.Collection{
|
||||
Name: "test",
|
||||
Schema: schema.NewSchema(
|
||||
&schema.SchemaField{
|
||||
Type: "text",
|
||||
Name: "fieldA",
|
||||
},
|
||||
&schema.SchemaField{
|
||||
Type: "text",
|
||||
Name: "fieldB",
|
||||
},
|
||||
&schema.SchemaField{
|
||||
Type: "text",
|
||||
Name: "fieldC",
|
||||
},
|
||||
),
|
||||
Indexes: types.JsonArray[string]{
|
||||
// the field case shouldn't matter
|
||||
"create unique index unique_single_idx on test (fielda)",
|
||||
"create unique index unique_combined_idx on test (fieldb, FIELDC)",
|
||||
},
|
||||
}
|
||||
if err := app.Dao().SaveCollection(collection); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dummyRecord := models.NewRecord(collection)
|
||||
dummyRecord.Set("fieldA", "a")
|
||||
dummyRecord.Set("fieldB", "b")
|
||||
dummyRecord.Set("fieldC", "c")
|
||||
if err := app.Dao().SaveRecord(dummyRecord); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
data map[string]any
|
||||
expectedErrors []string
|
||||
}{
|
||||
{
|
||||
"duplicated unique value",
|
||||
map[string]any{
|
||||
"fieldA": "a",
|
||||
},
|
||||
[]string{"fieldA"},
|
||||
},
|
||||
{
|
||||
"duplicated combined unique value",
|
||||
map[string]any{
|
||||
"fieldB": "b",
|
||||
"fieldC": "c",
|
||||
},
|
||||
[]string{"fieldB", "fieldC"},
|
||||
},
|
||||
{
|
||||
"non-duplicated unique value",
|
||||
map[string]any{
|
||||
"fieldA": "a2",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"non-duplicated combined unique value",
|
||||
map[string]any{
|
||||
"fieldB": "b",
|
||||
"fieldC": "d",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
record := models.NewRecord(collection)
|
||||
|
||||
form := forms.NewRecordUpsert(app, record)
|
||||
if err := form.LoadData(s.data); err != nil {
|
||||
t.Errorf("[%s] Failed to load form data", s.name)
|
||||
continue
|
||||
}
|
||||
|
||||
result := form.Submit()
|
||||
|
||||
// parse errors
|
||||
errs, ok := result.(validation.Errors)
|
||||
if !ok && result != nil {
|
||||
t.Errorf("[%s] Failed to parse errors %v", s.name, result)
|
||||
continue
|
||||
}
|
||||
|
||||
// check errors
|
||||
if len(errs) > len(s.expectedErrors) {
|
||||
t.Errorf("[%s] Expected error keys %v, got %v", s.name, s.expectedErrors, errs)
|
||||
continue
|
||||
}
|
||||
for _, k := range s.expectedErrors {
|
||||
if _, ok := errs[k]; !ok {
|
||||
t.Errorf("[%s] Missing expected error key %q in %v", s.name, k, errs)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +58,22 @@ func (form *SettingsUpsert) Submit(interceptors ...InterceptorFunc[*settings.Set
|
||||
return runInterceptors(form.Settings, func(s *settings.Settings) error {
|
||||
form.Settings = s
|
||||
|
||||
oldSettings, err := form.app.Settings().Clone();
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// eagerly merge the application settings with the form ones
|
||||
if err := form.app.Settings().Merge(form.Settings); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// persists settings change
|
||||
encryptionKey := os.Getenv(form.app.EncryptionEnv())
|
||||
if err := form.dao.SaveSettings(form.Settings, encryptionKey); err != nil {
|
||||
// try to revert app settings
|
||||
form.app.Settings().Merge(oldSettings)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -73,7 +87,6 @@ func (form *SettingsUpsert) Submit(interceptors ...InterceptorFunc[*settings.Set
|
||||
form.app.LogsDao().Vacuum()
|
||||
}
|
||||
|
||||
// merge the application settings with the form ones
|
||||
return form.app.Settings().Merge(form.Settings)
|
||||
return nil
|
||||
}, interceptors...)
|
||||
}
|
||||
@@ -24,56 +24,58 @@ func TestEmailSendValidateAndSubmit(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
func() {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
form := forms.NewTestEmailSend(app)
|
||||
form.Email = s.email
|
||||
form.Template = s.template
|
||||
form := forms.NewTestEmailSend(app)
|
||||
form.Email = s.email
|
||||
form.Template = s.template
|
||||
|
||||
result := form.Submit()
|
||||
result := form.Submit()
|
||||
|
||||
// parse errors
|
||||
errs, ok := result.(validation.Errors)
|
||||
if !ok && result != nil {
|
||||
t.Errorf("(%d) Failed to parse errors %v", i, result)
|
||||
continue
|
||||
}
|
||||
|
||||
// check errors
|
||||
if len(errs) > len(s.expectedErrors) {
|
||||
t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs)
|
||||
continue
|
||||
}
|
||||
for _, k := range s.expectedErrors {
|
||||
if _, ok := errs[k]; !ok {
|
||||
t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs)
|
||||
continue
|
||||
// parse errors
|
||||
errs, ok := result.(validation.Errors)
|
||||
if !ok && result != nil {
|
||||
t.Errorf("(%d) Failed to parse errors %v", i, result)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
expectedEmails := 1
|
||||
if len(s.expectedErrors) > 0 {
|
||||
expectedEmails = 0
|
||||
}
|
||||
// check errors
|
||||
if len(errs) > len(s.expectedErrors) {
|
||||
t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs)
|
||||
return
|
||||
}
|
||||
for _, k := range s.expectedErrors {
|
||||
if _, ok := errs[k]; !ok {
|
||||
t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if app.TestMailer.TotalSend != expectedEmails {
|
||||
t.Errorf("(%d) Expected %d email(s) to be sent, got %d", i, expectedEmails, app.TestMailer.TotalSend)
|
||||
}
|
||||
expectedEmails := 1
|
||||
if len(s.expectedErrors) > 0 {
|
||||
expectedEmails = 0
|
||||
}
|
||||
|
||||
if len(s.expectedErrors) > 0 {
|
||||
continue
|
||||
}
|
||||
if app.TestMailer.TotalSend != expectedEmails {
|
||||
t.Errorf("(%d) Expected %d email(s) to be sent, got %d", i, expectedEmails, app.TestMailer.TotalSend)
|
||||
}
|
||||
|
||||
expectedContent := "Verify"
|
||||
if s.template == "password-reset" {
|
||||
expectedContent = "Reset password"
|
||||
} else if s.template == "email-change" {
|
||||
expectedContent = "Confirm new email"
|
||||
}
|
||||
if len(s.expectedErrors) > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(app.TestMailer.LastMessage.HTML, expectedContent) {
|
||||
t.Errorf("(%d) Expected the email to contains %s, got \n%v", i, expectedContent, app.TestMailer.LastMessage.HTML)
|
||||
}
|
||||
expectedContent := "Verify"
|
||||
if s.template == "password-reset" {
|
||||
expectedContent = "Reset password"
|
||||
} else if s.template == "email-change" {
|
||||
expectedContent = "Confirm new email"
|
||||
}
|
||||
|
||||
if !strings.Contains(app.TestMailer.LastMessage.HTML, expectedContent) {
|
||||
t.Errorf("(%d) Expected the email to contains %s, got \n%v", i, expectedContent, app.TestMailer.LastMessage.HTML)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package forms
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/models/settings"
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
const (
|
||||
s3FilesystemStorage = "storage"
|
||||
s3FilesystemBackups = "backups"
|
||||
)
|
||||
|
||||
// TestS3Filesystem defines a S3 filesystem connection test.
|
||||
type TestS3Filesystem struct {
|
||||
app core.App
|
||||
|
||||
// The name of the filesystem - storage or backups
|
||||
Filesystem string `form:"filesystem" json:"filesystem"`
|
||||
}
|
||||
|
||||
// NewTestS3Filesystem creates and initializes new TestS3Filesystem form.
|
||||
func NewTestS3Filesystem(app core.App) *TestS3Filesystem {
|
||||
return &TestS3Filesystem{app: app}
|
||||
}
|
||||
|
||||
// Validate makes the form validatable by implementing [validation.Validatable] interface.
|
||||
func (form *TestS3Filesystem) Validate() error {
|
||||
return validation.ValidateStruct(form,
|
||||
validation.Field(
|
||||
&form.Filesystem,
|
||||
validation.Required,
|
||||
validation.In(s3FilesystemStorage, s3FilesystemBackups),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Submit validates and performs a S3 filesystem connection test.
|
||||
func (form *TestS3Filesystem) Submit() error {
|
||||
if err := form.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var s3Config settings.S3Config
|
||||
|
||||
if form.Filesystem == s3FilesystemBackups {
|
||||
s3Config = form.app.Settings().Backups.S3
|
||||
} else {
|
||||
s3Config = form.app.Settings().S3
|
||||
}
|
||||
|
||||
if !s3Config.Enabled {
|
||||
return errors.New("S3 storage filesystem is not enabled")
|
||||
}
|
||||
|
||||
fsys, err := filesystem.NewS3(
|
||||
s3Config.Bucket,
|
||||
s3Config.Region,
|
||||
s3Config.Endpoint,
|
||||
s3Config.AccessKey,
|
||||
s3Config.Secret,
|
||||
s3Config.ForcePathStyle,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize the S3 filesystem: %w", err)
|
||||
}
|
||||
defer fsys.Close()
|
||||
|
||||
testPrefix := "pb_settings_test_" + security.PseudorandomString(5)
|
||||
testFileKey := testPrefix + "/test.txt"
|
||||
|
||||
// try to upload a test file
|
||||
if err := fsys.Upload([]byte("test"), testFileKey); err != nil {
|
||||
return fmt.Errorf("failed to upload a test file: %w", err)
|
||||
}
|
||||
|
||||
// test prefix deletion (ensures that both bucket list and delete works)
|
||||
if errs := fsys.DeletePrefix(testPrefix); len(errs) > 0 {
|
||||
return fmt.Errorf("failed to delete a test file: %w", errs[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package forms_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/pocketbase/forms"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
)
|
||||
|
||||
func TestS3FilesystemValidate(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
filesystem string
|
||||
expectedErrors []string
|
||||
}{
|
||||
{
|
||||
"empty filesystem",
|
||||
"",
|
||||
[]string{"filesystem"},
|
||||
},
|
||||
{
|
||||
"invalid filesystem",
|
||||
"something",
|
||||
[]string{"filesystem"},
|
||||
},
|
||||
{
|
||||
"backups filesystem",
|
||||
"backups",
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"storage filesystem",
|
||||
"storage",
|
||||
[]string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
form := forms.NewTestS3Filesystem(app)
|
||||
form.Filesystem = s.filesystem
|
||||
|
||||
result := form.Validate()
|
||||
|
||||
// parse errors
|
||||
errs, ok := result.(validation.Errors)
|
||||
if !ok && result != nil {
|
||||
t.Errorf("[%s] Failed to parse errors %v", s.name, result)
|
||||
continue
|
||||
}
|
||||
|
||||
// check errors
|
||||
if len(errs) > len(s.expectedErrors) {
|
||||
t.Errorf("[%s] Expected error keys %v, got %v", s.name, s.expectedErrors, errs)
|
||||
continue
|
||||
}
|
||||
for _, k := range s.expectedErrors {
|
||||
if _, ok := errs[k]; !ok {
|
||||
t.Errorf("[%s] Missing expected error key %q in %v", s.name, k, errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3FilesystemSubmitFailure(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
// check if validate was called
|
||||
{
|
||||
form := forms.NewTestS3Filesystem(app)
|
||||
form.Filesystem = ""
|
||||
|
||||
result := form.Submit()
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
|
||||
if _, ok := result.(validation.Errors); !ok {
|
||||
t.Fatalf("Expected validation.Error, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
// check with valid storage and disabled s3
|
||||
{
|
||||
form := forms.NewTestS3Filesystem(app)
|
||||
form.Filesystem = "storage"
|
||||
|
||||
result := form.Submit()
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
|
||||
if _, ok := result.(validation.Error); ok {
|
||||
t.Fatalf("Didn't expect validation.Error, got %v", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,88 +3,89 @@ module github.com/pocketbase/pocketbase
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6
|
||||
github.com/aws/aws-sdk-go v1.44.249
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/aws/aws-sdk-go v1.44.298
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/domodwyer/mailyak/v3 v3.6.0
|
||||
github.com/dop251/goja v0.0.0-20221118162653-d4bf6fde1b86
|
||||
github.com/dop251/goja_nodejs v0.0.0-20221009164102-3aa5028e57f6
|
||||
github.com/dop251/goja v0.0.0-20230427124612-428fc442ff5f
|
||||
github.com/dop251/goja_nodejs v0.0.0-20230322100729-2550c7b6c124
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.2
|
||||
github.com/ganigeorgiev/fexpr v0.3.0
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/mattn/go-sqlite3 v1.14.17
|
||||
github.com/pocketbase/dbx v1.10.0
|
||||
github.com/spf13/cast v1.5.0
|
||||
github.com/spf13/cast v1.5.1
|
||||
github.com/spf13/cobra v1.7.0
|
||||
gocloud.dev v0.29.0
|
||||
golang.org/x/crypto v0.8.0
|
||||
golang.org/x/net v0.9.0
|
||||
golang.org/x/oauth2 v0.7.0
|
||||
modernc.org/sqlite v1.22.0
|
||||
gocloud.dev v0.30.0
|
||||
golang.org/x/crypto v0.11.0
|
||||
golang.org/x/net v0.12.0
|
||||
golang.org/x/oauth2 v0.10.0
|
||||
modernc.org/sqlite v1.23.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.64 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.26 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.71 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 // indirect
|
||||
github.com/aws/smithy-go v1.13.5 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/pprof v0.0.0-20230510103437-eeec1cb781c3 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/google/wire v0.5.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.8.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/image v0.7.0 // indirect
|
||||
golang.org/x/mod v0.10.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/term v0.7.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
golang.org/x/image v0.9.0 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/term v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.8.0 // indirect
|
||||
golang.org/x/tools v0.11.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/api v0.119.0 // indirect
|
||||
google.golang.org/api v0.130.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
google.golang.org/grpc v1.54.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 // indirect
|
||||
google.golang.org/grpc v1.56.2 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
lukechampine.com/uint128 v1.3.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/cc/v3 v3.41.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.14 // indirect
|
||||
modernc.org/libc v1.24.1 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.6.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
"github.com/pocketbase/pocketbase/models/settings"
|
||||
"github.com/pocketbase/pocketbase/tools/migrate"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
@@ -90,6 +91,15 @@ func init() {
|
||||
return tablesErr
|
||||
}
|
||||
|
||||
dao := daos.New(db)
|
||||
|
||||
// inserts default settings
|
||||
// -----------------------------------------------------------
|
||||
defaultSettings := settings.New()
|
||||
if err := dao.SaveSettings(defaultSettings); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// inserts the system profiles collection
|
||||
// -----------------------------------------------------------
|
||||
usersCollection := &models.Collection{}
|
||||
@@ -139,7 +149,7 @@ func init() {
|
||||
},
|
||||
)
|
||||
|
||||
return daos.New(db).SaveCollection(usersCollection)
|
||||
return dao.SaveCollection(usersCollection)
|
||||
}, func(db dbx.Builder) error {
|
||||
tables := []string{
|
||||
"users",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
||||
// Cleanup dangling deleted collections references
|
||||
// (see https://github.com/pocketbase/pocketbase/discussions/2570).
|
||||
func init() {
|
||||
AppMigrations.Register(func(db dbx.Builder) error {
|
||||
_, err := db.NewQuery(`
|
||||
DELETE FROM {{_externalAuths}}
|
||||
WHERE [[collectionId]] NOT IN (SELECT [[id]] FROM {{_collections}})
|
||||
`).Execute()
|
||||
|
||||
return err
|
||||
}, func(db dbx.Builder) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
import "github.com/pocketbase/pocketbase/tools/types"
|
||||
|
||||
type BackupFileInfo struct {
|
||||
Key string `json:"key"`
|
||||
Size int64 `json:"size"`
|
||||
Modified types.DateTime `json:"modified"`
|
||||
}
|
||||
@@ -190,7 +190,7 @@ func (f SchemaField) Validate() error {
|
||||
|
||||
excludeNames := BaseModelFieldNames()
|
||||
// exclude special filter literals
|
||||
excludeNames = append(excludeNames, "null", "true", "false")
|
||||
excludeNames = append(excludeNames, "null", "true", "false", "_rowid_")
|
||||
// exclude system literals
|
||||
excludeNames = append(excludeNames, SystemFieldNames()...)
|
||||
|
||||
|
||||
@@ -313,6 +313,15 @@ func TestSchemaFieldValidate(t *testing.T) {
|
||||
},
|
||||
[]string{"name"},
|
||||
},
|
||||
{
|
||||
"reserved name (_rowid_)",
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeText,
|
||||
Id: "1234567890",
|
||||
Name: "_rowid_",
|
||||
},
|
||||
[]string{"name"},
|
||||
},
|
||||
{
|
||||
"reserved name (id)",
|
||||
schema.SchemaField{
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||
"github.com/pocketbase/pocketbase/tools/auth"
|
||||
"github.com/pocketbase/pocketbase/tools/cron"
|
||||
"github.com/pocketbase/pocketbase/tools/mailer"
|
||||
"github.com/pocketbase/pocketbase/tools/rest"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
@@ -23,10 +24,11 @@ const SecretMask string = "******"
|
||||
type Settings struct {
|
||||
mux sync.RWMutex
|
||||
|
||||
Meta MetaConfig `form:"meta" json:"meta"`
|
||||
Logs LogsConfig `form:"logs" json:"logs"`
|
||||
Smtp SmtpConfig `form:"smtp" json:"smtp"`
|
||||
S3 S3Config `form:"s3" json:"s3"`
|
||||
Meta MetaConfig `form:"meta" json:"meta"`
|
||||
Logs LogsConfig `form:"logs" json:"logs"`
|
||||
Smtp SmtpConfig `form:"smtp" json:"smtp"`
|
||||
S3 S3Config `form:"s3" json:"s3"`
|
||||
Backups BackupsConfig `form:"backups" json:"backups"`
|
||||
|
||||
AdminAuthToken TokenConfig `form:"adminAuthToken" json:"adminAuthToken"`
|
||||
AdminPasswordResetToken TokenConfig `form:"adminPasswordResetToken" json:"adminPasswordResetToken"`
|
||||
@@ -84,6 +86,9 @@ func New() *Settings {
|
||||
Password: "",
|
||||
Tls: false,
|
||||
},
|
||||
Backups: BackupsConfig{
|
||||
CronMaxKeep: 3,
|
||||
},
|
||||
AdminAuthToken: TokenConfig{
|
||||
Secret: security.RandomString(50),
|
||||
Duration: 1209600, // 14 days
|
||||
@@ -94,7 +99,7 @@ func New() *Settings {
|
||||
},
|
||||
AdminFileToken: TokenConfig{
|
||||
Secret: security.RandomString(50),
|
||||
Duration: 300, // 5 minutes
|
||||
Duration: 120, // 2 minutes
|
||||
},
|
||||
RecordAuthToken: TokenConfig{
|
||||
Secret: security.RandomString(50),
|
||||
@@ -110,7 +115,7 @@ func New() *Settings {
|
||||
},
|
||||
RecordFileToken: TokenConfig{
|
||||
Secret: security.RandomString(50),
|
||||
Duration: 300, // 5 minutes
|
||||
Duration: 120, // 2 minutes
|
||||
},
|
||||
RecordEmailChangeToken: TokenConfig{
|
||||
Secret: security.RandomString(50),
|
||||
@@ -191,6 +196,7 @@ func (s *Settings) Validate() error {
|
||||
validation.Field(&s.RecordFileToken),
|
||||
validation.Field(&s.Smtp),
|
||||
validation.Field(&s.S3),
|
||||
validation.Field(&s.Backups),
|
||||
validation.Field(&s.GoogleAuth),
|
||||
validation.Field(&s.FacebookAuth),
|
||||
validation.Field(&s.GithubAuth),
|
||||
@@ -245,6 +251,7 @@ func (s *Settings) RedactClone() (*Settings, error) {
|
||||
sensitiveFields := []*string{
|
||||
&clone.Smtp.Password,
|
||||
&clone.S3.Secret,
|
||||
&clone.Backups.S3.Secret,
|
||||
&clone.AdminAuthToken.Secret,
|
||||
&clone.AdminPasswordResetToken.Secret,
|
||||
&clone.AdminFileToken.Secret,
|
||||
@@ -393,6 +400,51 @@ func (c S3Config) Validate() error {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type BackupsConfig struct {
|
||||
// Cron is a cron expression to schedule auto backups, eg. "* * * * *".
|
||||
//
|
||||
// Leave it empty to disable the auto backups functionality.
|
||||
Cron string `form:"cron" json:"cron"`
|
||||
|
||||
// CronMaxKeep is the the max number of cron generated backups to
|
||||
// keep before removing older entries.
|
||||
//
|
||||
// This field works only when the cron config has valid cron expression.
|
||||
CronMaxKeep int `form:"cronMaxKeep" json:"cronMaxKeep"`
|
||||
|
||||
// S3 is an optional S3 storage config specifying where to store the app backups.
|
||||
S3 S3Config `form:"s3" json:"s3"`
|
||||
}
|
||||
|
||||
// Validate makes BackupsConfig validatable by implementing [validation.Validatable] interface.
|
||||
func (c BackupsConfig) Validate() error {
|
||||
return validation.ValidateStruct(&c,
|
||||
validation.Field(&c.S3),
|
||||
validation.Field(&c.Cron, validation.By(checkCronExpression)),
|
||||
validation.Field(
|
||||
&c.CronMaxKeep,
|
||||
validation.When(c.Cron != "", validation.Required),
|
||||
validation.Min(1),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func checkCronExpression(value any) error {
|
||||
v, _ := value.(string)
|
||||
if v == "" {
|
||||
return nil // nothing to check
|
||||
}
|
||||
|
||||
_, err := cron.NewSchedule(v)
|
||||
if err != nil {
|
||||
return validation.NewError("validation_invalid_cron", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type MetaConfig struct {
|
||||
AppName string `form:"appName" json:"appName"`
|
||||
AppUrl string `form:"appUrl" json:"appUrl"`
|
||||
|
||||
@@ -127,6 +127,7 @@ func TestSettingsMerge(t *testing.T) {
|
||||
s2.Smtp.Enabled = true
|
||||
s2.S3.Enabled = true
|
||||
s2.S3.Endpoint = "test"
|
||||
s2.Backups.Cron = "* * * * *"
|
||||
s2.AdminAuthToken.Duration = 1
|
||||
s2.AdminPasswordResetToken.Duration = 2
|
||||
s2.AdminFileToken.Duration = 2
|
||||
@@ -231,6 +232,7 @@ func TestSettingsRedactClone(t *testing.T) {
|
||||
// secrets
|
||||
s1.Smtp.Password = testSecret
|
||||
s1.S3.Secret = testSecret
|
||||
s1.Backups.S3.Secret = testSecret
|
||||
s1.AdminAuthToken.Secret = testSecret
|
||||
s1.AdminPasswordResetToken.Secret = testSecret
|
||||
s1.AdminFileToken.Secret = testSecret
|
||||
@@ -610,6 +612,74 @@ func TestMetaConfigValidate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupsConfigValidate(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
config settings.BackupsConfig
|
||||
expectedErrors []string
|
||||
}{
|
||||
{
|
||||
"zero value",
|
||||
settings.BackupsConfig{},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"invalid cron",
|
||||
settings.BackupsConfig{
|
||||
Cron: "invalid",
|
||||
CronMaxKeep: 0,
|
||||
},
|
||||
[]string{"cron", "cronMaxKeep"},
|
||||
},
|
||||
{
|
||||
"invalid enabled S3",
|
||||
settings.BackupsConfig{
|
||||
S3: settings.S3Config{
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
[]string{"s3"},
|
||||
},
|
||||
{
|
||||
"valid data",
|
||||
settings.BackupsConfig{
|
||||
S3: settings.S3Config{
|
||||
Enabled: true,
|
||||
Endpoint: "example.com",
|
||||
Bucket: "test",
|
||||
Region: "test",
|
||||
AccessKey: "test",
|
||||
Secret: "test",
|
||||
},
|
||||
Cron: "*/10 * * * *",
|
||||
CronMaxKeep: 1,
|
||||
},
|
||||
[]string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
result := s.config.Validate()
|
||||
|
||||
// parse errors
|
||||
errs, ok := result.(validation.Errors)
|
||||
if !ok && result != nil {
|
||||
t.Errorf("[%s] Failed to parse errors %v", s.name, result)
|
||||
continue
|
||||
}
|
||||
|
||||
// check errors
|
||||
if len(errs) > len(s.expectedErrors) {
|
||||
t.Errorf("[%s] Expected error keys %v, got %v", s.name, s.expectedErrors, errs)
|
||||
}
|
||||
for _, k := range s.expectedErrors {
|
||||
if _, ok := errs[k]; !ok {
|
||||
t.Errorf("[%s] Missing expected error key %q in %v", s.name, k, errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailTemplateValidate(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
emailTemplate settings.EmailTemplate
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
// Package ghupdate implements a new command to selfupdate the current
|
||||
// PocketBase executable with the latest GitHub release.
|
||||
package ghupdate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/archive"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// HttpClient is a base HTTP client interface (usually used for test purposes).
|
||||
type HttpClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// Options defines optional struct to customize the default plugin behavior.
|
||||
//
|
||||
// NB! This plugin is considered experimental and its options may change in the future.
|
||||
type Options struct {
|
||||
// Owner specifies the account owner of the repository (default to "pocketbase").
|
||||
Owner string
|
||||
|
||||
// Repo specifies the name of the repository (default to "pocketbase").
|
||||
Repo string
|
||||
|
||||
// ArchiveExecutable specifies the name of the executable file in the release archive
|
||||
// (default to "pocketbase"; an additional ".exe" check is also performed as a fallback).
|
||||
ArchiveExecutable string
|
||||
|
||||
// Optional context to use when fetching and downloading the latest release.
|
||||
Context context.Context
|
||||
|
||||
// The HTTP client to use when fetching and downloading the latest release.
|
||||
// Defaults to `http.DefaultClient`.
|
||||
HttpClient HttpClient
|
||||
}
|
||||
|
||||
// MustRegister registers the ghupdate plugin to the provided app instance
|
||||
// and panic if it fails.
|
||||
func MustRegister(app core.App, rootCmd *cobra.Command, options *Options) {
|
||||
if err := Register(app, rootCmd, options); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Register registers the ghupdate plugin to the provided app instance.
|
||||
func Register(app core.App, rootCmd *cobra.Command, options *Options) error {
|
||||
p := &plugin{
|
||||
app: app,
|
||||
currentVersion: rootCmd.Version,
|
||||
}
|
||||
|
||||
if options != nil {
|
||||
p.options = options
|
||||
} else {
|
||||
p.options = &Options{}
|
||||
}
|
||||
|
||||
if p.options.Owner == "" {
|
||||
p.options.Owner = "pocketbase"
|
||||
}
|
||||
|
||||
if p.options.Repo == "" {
|
||||
p.options.Repo = "pocketbase"
|
||||
}
|
||||
|
||||
if p.options.ArchiveExecutable == "" {
|
||||
p.options.ArchiveExecutable = "pocketbase"
|
||||
}
|
||||
|
||||
if p.options.HttpClient == nil {
|
||||
p.options.HttpClient = http.DefaultClient
|
||||
}
|
||||
|
||||
if p.options.Context == nil {
|
||||
p.options.Context = context.Background()
|
||||
}
|
||||
|
||||
rootCmd.AddCommand(p.updateCmd())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type plugin struct {
|
||||
app core.App
|
||||
currentVersion string
|
||||
options *Options
|
||||
}
|
||||
|
||||
func (p *plugin) updateCmd() *cobra.Command {
|
||||
var withBackup bool
|
||||
|
||||
command := &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Automatically updates the current PocketBase executable with the latest available version",
|
||||
// @todo remove after logs generalization
|
||||
// prevents printing the error log twice
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
return p.update(withBackup)
|
||||
},
|
||||
}
|
||||
|
||||
command.PersistentFlags().BoolVar(
|
||||
&withBackup,
|
||||
"backup",
|
||||
true,
|
||||
"Creates a pb_data backup at the end of the update process",
|
||||
)
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func (p *plugin) update(withBackup bool) error {
|
||||
color.Yellow("Fetching release information...")
|
||||
|
||||
latest, err := fetchLatestRelease(
|
||||
p.options.Context,
|
||||
p.options.HttpClient,
|
||||
p.options.Owner,
|
||||
p.options.Repo,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if compareVersions(strings.TrimPrefix(p.currentVersion, "v"), strings.TrimPrefix(latest.Tag, "v")) <= 0 {
|
||||
color.Green("You already have the latest PocketBase %s.", p.currentVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
suffix := archiveSuffix(runtime.GOOS, runtime.GOARCH)
|
||||
if suffix == "" {
|
||||
return errors.New("unsupported platform")
|
||||
}
|
||||
|
||||
asset, err := latest.findAssetBySuffix(suffix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
releaseDir := filepath.Join(p.app.DataDir(), core.LocalTempDirName)
|
||||
defer os.RemoveAll(releaseDir)
|
||||
|
||||
color.Yellow("Downloading %s...", asset.Name)
|
||||
|
||||
// download the release asset
|
||||
assetZip := filepath.Join(releaseDir, asset.Name)
|
||||
if err := downloadFile(p.options.Context, p.options.HttpClient, asset.DownloadUrl, assetZip); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Yellow("Extracting %s...", asset.Name)
|
||||
|
||||
extractDir := filepath.Join(releaseDir, "extracted_"+asset.Name)
|
||||
defer os.RemoveAll(extractDir)
|
||||
|
||||
if err := archive.Extract(assetZip, extractDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Yellow("Replacing the executable...")
|
||||
|
||||
oldExec, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
renamedOldExec := oldExec + ".old"
|
||||
defer os.Remove(renamedOldExec)
|
||||
|
||||
newExec := filepath.Join(extractDir, p.options.ArchiveExecutable)
|
||||
if _, err := os.Stat(newExec); err != nil {
|
||||
// try again with an .exe extension
|
||||
newExec = newExec + ".exe"
|
||||
if _, fallbackErr := os.Stat(newExec); fallbackErr != nil {
|
||||
return fmt.Errorf("The executable in the extracted path is missing or it is inaccessible: %v, %v", err, fallbackErr)
|
||||
}
|
||||
}
|
||||
|
||||
// rename the current executable
|
||||
if err := os.Rename(oldExec, renamedOldExec); err != nil {
|
||||
return fmt.Errorf("Failed to rename the current executable: %w", err)
|
||||
}
|
||||
|
||||
tryToRevertExecChanges := func() {
|
||||
if revertErr := os.Rename(renamedOldExec, oldExec); revertErr != nil && p.app.IsDebug() {
|
||||
log.Println(revertErr)
|
||||
}
|
||||
}
|
||||
|
||||
// replace with the extracted binary
|
||||
if err := os.Rename(newExec, oldExec); err != nil {
|
||||
tryToRevertExecChanges()
|
||||
return fmt.Errorf("Failed replacing the executable: %w", err)
|
||||
}
|
||||
|
||||
if withBackup {
|
||||
color.Yellow("Creating pb_data backup...")
|
||||
|
||||
backupName := fmt.Sprintf("@update_%s.zip", latest.Tag)
|
||||
if err := p.app.CreateBackup(p.options.Context, backupName); err != nil {
|
||||
tryToRevertExecChanges()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
color.HiBlack("---")
|
||||
color.Green("Update completed sucessfully! You can start the executable as usual.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchLatestRelease(
|
||||
ctx context.Context,
|
||||
client HttpClient,
|
||||
owner string,
|
||||
repo string,
|
||||
) (*release, error) {
|
||||
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
rawBody, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// http.Client doesn't treat non 2xx responses as error
|
||||
if res.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf(
|
||||
"(%d) failed to fetch latest releases:\n%s",
|
||||
res.StatusCode,
|
||||
string(rawBody),
|
||||
)
|
||||
}
|
||||
|
||||
result := &release{}
|
||||
if err := json.Unmarshal(rawBody, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func downloadFile(
|
||||
ctx context.Context,
|
||||
client HttpClient,
|
||||
url string,
|
||||
destPath string,
|
||||
) error {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
// http.Client doesn't treat non 2xx responses as error
|
||||
if res.StatusCode >= 400 {
|
||||
return fmt.Errorf("(%d) failed to send download file request", res.StatusCode)
|
||||
}
|
||||
|
||||
// ensure that the dest parent dir(s) exist
|
||||
if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dest, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dest.Close()
|
||||
|
||||
if _, err := io.Copy(dest, res.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func archiveSuffix(goos, goarch string) string {
|
||||
switch goos {
|
||||
case "linux":
|
||||
switch goarch {
|
||||
case "amd64":
|
||||
return "_linux_amd64.zip"
|
||||
case "arm64":
|
||||
return "_linux_arm64.zip"
|
||||
case "arm":
|
||||
return "_linux_armv7.zip"
|
||||
}
|
||||
case "darwin":
|
||||
switch goarch {
|
||||
case "amd64":
|
||||
return "_darwin_amd64.zip"
|
||||
case "arm64":
|
||||
return "_darwin_arm64.zip"
|
||||
}
|
||||
case "windows":
|
||||
switch goarch {
|
||||
case "amd64":
|
||||
return "_windows_amd64.zip"
|
||||
case "arm64":
|
||||
return "_windows_arm64.zip"
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func compareVersions(a, b string) int {
|
||||
aSplit := strings.Split(a, ".")
|
||||
aTotal := len(aSplit)
|
||||
|
||||
bSplit := strings.Split(b, ".")
|
||||
bTotal := len(bSplit)
|
||||
|
||||
limit := aTotal
|
||||
if bTotal > aTotal {
|
||||
limit = bTotal
|
||||
}
|
||||
|
||||
for i := 0; i < limit; i++ {
|
||||
var x, y int
|
||||
|
||||
if i < aTotal {
|
||||
x, _ = strconv.Atoi(aSplit[i])
|
||||
}
|
||||
|
||||
if i < bTotal {
|
||||
y, _ = strconv.Atoi(bSplit[i])
|
||||
}
|
||||
|
||||
if x < y {
|
||||
return 1 // b is newer
|
||||
}
|
||||
|
||||
if x > y {
|
||||
return -1 // a is newer
|
||||
}
|
||||
}
|
||||
|
||||
return 0 // equal
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package ghupdate
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
a string
|
||||
b string
|
||||
expected int
|
||||
}{
|
||||
{"", "", 0},
|
||||
{"0", "", 0},
|
||||
{"1", "1.0.0", 0},
|
||||
{"1.1", "1.1.0", 0},
|
||||
{"1.1", "1.1.1", 1},
|
||||
{"1.1", "1.0.1", -1},
|
||||
{"1.0", "1.0.1", 1},
|
||||
{"1.10", "1.9", -1},
|
||||
{"1.2", "1.12", 1},
|
||||
{"3.2", "1.6", -1},
|
||||
{"3.2", "4.0", 1},
|
||||
{"3.2.4", "3.2.3", -1},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
result := compareVersions(s.a, s.b)
|
||||
|
||||
if result != s.expected {
|
||||
t.Fatalf("[%d] Expected %q vs %q to result in %d, got %d", i, s.a, s.b, s.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package ghupdate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type releaseAsset struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Size int `json:"size"`
|
||||
DownloadUrl string `json:"browser_download_url"`
|
||||
}
|
||||
|
||||
type release struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Tag string `json:"tag_name"`
|
||||
Published string `json:"published_at"`
|
||||
Url string `json:"html_url"`
|
||||
Assets []*releaseAsset `json:"assets"`
|
||||
}
|
||||
|
||||
// findAssetBySuffix returns the first available asset containing the specified suffix.
|
||||
func (r *release) findAssetBySuffix(suffix string) (*releaseAsset, error) {
|
||||
if suffix != "" {
|
||||
for _, asset := range r.Assets {
|
||||
if strings.HasSuffix(asset.Name, suffix) {
|
||||
return asset, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("missing asset containing " + suffix)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package ghupdate
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestReleaseFindAssetBySuffix(t *testing.T) {
|
||||
r := release{
|
||||
Assets: []*releaseAsset{
|
||||
{Name: "test1.zip", Id: 1},
|
||||
{Name: "test2.zip", Id: 2},
|
||||
{Name: "test22.zip", Id: 22},
|
||||
{Name: "test3.zip", Id: 3},
|
||||
},
|
||||
}
|
||||
|
||||
asset, err := r.findAssetBySuffix("2.zip")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected nil, got err: %v", err)
|
||||
}
|
||||
|
||||
if asset.Id != 2 {
|
||||
t.Fatalf("Expected asset with id %d, got %v", 2, asset)
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,13 @@ package migratecmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
@@ -103,7 +103,19 @@ func Register(app core.App, rootCmd *cobra.Command, options *Options) error {
|
||||
// migrations but there is already at least 1 collection created,
|
||||
// to ensure that the automigrate will work with up-to-date collections data
|
||||
if !p.hasCustomMigrations() && len(cachedCollections) > 1 {
|
||||
p.migrateCollectionsHandler(nil, false)
|
||||
snapshotFile, err := p.migrateCollectionsHandler(nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// insert the snapshot migration entry
|
||||
_, insertErr := p.app.Dao().NonconcurrentDB().Insert(migrate.DefaultMigrationsTable, dbx.Params{
|
||||
"file": snapshotFile,
|
||||
"applied": time.Now().Unix(),
|
||||
}).Execute()
|
||||
if insertErr != nil {
|
||||
return insertErr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -129,9 +141,12 @@ func (p *plugin) createCommand() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "migrate",
|
||||
Short: "Executes app DB migration scripts",
|
||||
ValidArgs: []string{"up", "down", "create", "collections"},
|
||||
Long: cmdDesc,
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
ValidArgs: []string{"up", "down", "create", "collections"},
|
||||
// prevents printing the error log twice
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd := ""
|
||||
if len(args) > 0 {
|
||||
cmd = args[0]
|
||||
@@ -139,41 +154,42 @@ func (p *plugin) createCommand() *cobra.Command {
|
||||
|
||||
switch cmd {
|
||||
case "create":
|
||||
if err := p.migrateCreateHandler("", args[1:], true); err != nil {
|
||||
log.Fatal(err)
|
||||
if _, err := p.migrateCreateHandler("", args[1:], true); err != nil {
|
||||
return err
|
||||
}
|
||||
case "collections":
|
||||
if err := p.migrateCollectionsHandler(args[1:], true); err != nil {
|
||||
log.Fatal(err)
|
||||
if _, err := p.migrateCollectionsHandler(args[1:], true); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
runner, err := migrate.NewRunner(p.app.DB(), migrations.AppMigrations)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := runner.Run(args...); err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func (p *plugin) migrateCreateHandler(template string, args []string, interactive bool) error {
|
||||
func (p *plugin) migrateCreateHandler(template string, args []string, interactive bool) (string, error) {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("Missing migration file name")
|
||||
return "", fmt.Errorf("Missing migration file name")
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
dir := p.options.Dir
|
||||
|
||||
resultFilePath := path.Join(
|
||||
dir,
|
||||
fmt.Sprintf("%d_%s.%s", time.Now().Unix(), inflector.Snakecase(name), p.options.TemplateLang),
|
||||
)
|
||||
filename := fmt.Sprintf("%d_%s.%s", time.Now().Unix(), inflector.Snakecase(name), p.options.TemplateLang)
|
||||
|
||||
resultFilePath := path.Join(dir, filename)
|
||||
|
||||
if interactive {
|
||||
confirm := false
|
||||
@@ -183,7 +199,7 @@ func (p *plugin) migrateCreateHandler(template string, args []string, interactiv
|
||||
survey.AskOne(prompt, &confirm)
|
||||
if !confirm {
|
||||
fmt.Println("The command has been cancelled")
|
||||
return nil
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,34 +212,34 @@ func (p *plugin) migrateCreateHandler(template string, args []string, interactiv
|
||||
template, templateErr = p.goBlankTemplate()
|
||||
}
|
||||
if templateErr != nil {
|
||||
return fmt.Errorf("Failed to resolve create template: %v\n", templateErr)
|
||||
return "", fmt.Errorf("Failed to resolve create template: %v\n", templateErr)
|
||||
}
|
||||
}
|
||||
|
||||
// ensure that the migrations dir exist
|
||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
// save the migration file
|
||||
if err := os.WriteFile(resultFilePath, []byte(template), 0644); err != nil {
|
||||
return fmt.Errorf("Failed to save migration file %q: %v\n", resultFilePath, err)
|
||||
return "", fmt.Errorf("Failed to save migration file %q: %v\n", resultFilePath, err)
|
||||
}
|
||||
|
||||
if interactive {
|
||||
fmt.Printf("Successfully created file %q\n", resultFilePath)
|
||||
}
|
||||
|
||||
return nil
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
func (p *plugin) migrateCollectionsHandler(args []string, interactive bool) error {
|
||||
func (p *plugin) migrateCollectionsHandler(args []string, interactive bool) (string, error) {
|
||||
createArgs := []string{"collections_snapshot"}
|
||||
createArgs = append(createArgs, args...)
|
||||
|
||||
collections := []*models.Collection{}
|
||||
if err := p.app.Dao().CollectionQuery().OrderBy("created ASC").All(&collections); err != nil {
|
||||
return fmt.Errorf("Failed to fetch migrations list: %v", err)
|
||||
return "", fmt.Errorf("Failed to fetch migrations list: %v", err)
|
||||
}
|
||||
|
||||
var template string
|
||||
@@ -234,7 +250,7 @@ func (p *plugin) migrateCollectionsHandler(args []string, interactive bool) erro
|
||||
template, templateErr = p.goSnapshotTemplate(collections)
|
||||
}
|
||||
if templateErr != nil {
|
||||
return fmt.Errorf("Failed to resolve template: %v", templateErr)
|
||||
return "", fmt.Errorf("Failed to resolve template: %v", templateErr)
|
||||
}
|
||||
|
||||
return p.migrateCreateHandler(template, createArgs, interactive)
|
||||
|
||||
@@ -6,11 +6,14 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"github.com/pocketbase/pocketbase/tools/migrate"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
@@ -776,3 +779,36 @@ func TestAutomigrateCollectionNoChanges(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitialAutoSnapshot(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
migrationsDir := filepath.Join(app.DataDir(), "_test_auto_snapshot_")
|
||||
|
||||
migratecmd.MustRegister(app, nil, &migratecmd.Options{
|
||||
TemplateLang: migratecmd.TemplateLangJS,
|
||||
Automigrate: true,
|
||||
Dir: migrationsDir,
|
||||
})
|
||||
|
||||
app.Bootstrap()
|
||||
|
||||
app.OnBeforeServe().Trigger(&core.ServeEvent{
|
||||
App: app,
|
||||
})
|
||||
|
||||
var foundFiles []string
|
||||
|
||||
err := app.Dao().NonconcurrentDB().Select("file").
|
||||
From(migrate.DefaultMigrationsTable).
|
||||
Where(dbx.NewExp("file like '%collections_snapshot.js'")).
|
||||
Column(&foundFiles)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(foundFiles) != 1 {
|
||||
t.Fatalf("Expected 1 collections_snapshot migration, found %v", foundFiles)
|
||||
}
|
||||
}
|
||||
+19
-22
@@ -1,14 +1,13 @@
|
||||
package pocketbase
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/pocketbase/pocketbase/cmd"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
@@ -136,8 +135,8 @@ func NewWithConfig(config *Config) *PocketBase {
|
||||
// commands (serve, migrate, version) and executes pb.RootCmd.
|
||||
func (pb *PocketBase) Start() error {
|
||||
// register system commands
|
||||
pb.RootCmd.AddCommand(cmd.NewAdminCommand(pb))
|
||||
pb.RootCmd.AddCommand(cmd.NewServeCommand(pb, !pb.hideStartBanner))
|
||||
pb.RootCmd.AddCommand(cmd.NewTempUpgradeCommand(pb))
|
||||
|
||||
return pb.Execute()
|
||||
}
|
||||
@@ -154,35 +153,33 @@ func (pb *PocketBase) Execute() error {
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
done := make(chan bool, 1)
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
// wait for interrupt signal to gracefully shutdown the application
|
||||
// listen for interrupt signal to gracefully shutdown the application
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
quit := make(chan os.Signal, 1) // we need to reserve to buffer size 1, so the notifier are not blocked
|
||||
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||
<-quit
|
||||
sigch := make(chan os.Signal, 1)
|
||||
signal.Notify(sigch, os.Interrupt, syscall.SIGTERM)
|
||||
<-sigch
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// execute the root command
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := pb.RootCmd.Execute(); err != nil {
|
||||
log.Println(err)
|
||||
// @todo replace with db log once generalized logs are added
|
||||
// (note may need to update the existing commands to not silence errors)
|
||||
color.Red(err.Error())
|
||||
}
|
||||
|
||||
done <- true
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
<-done
|
||||
|
||||
// cleanup
|
||||
return pb.onTerminate()
|
||||
}
|
||||
|
||||
// onTerminate tries to release the app resources on app termination.
|
||||
func (pb *PocketBase) onTerminate() error {
|
||||
return pb.ResetBootstrapState()
|
||||
// trigger app cleanups
|
||||
return pb.OnTerminate().Trigger(&core.TerminateEvent{
|
||||
App: pb,
|
||||
})
|
||||
}
|
||||
|
||||
// eagerParseFlags parses the global app flags before calling pb.RootCmd.Execute().
|
||||
@@ -212,7 +209,7 @@ func (pb *PocketBase) eagerParseFlags(config *Config) error {
|
||||
return pb.RootCmd.ParseFlags(os.Args[1:])
|
||||
}
|
||||
|
||||
// skipBootstrap eagerly checks if the app should skip the bootstap process:
|
||||
// skipBootstrap eagerly checks if the app should skip the bootstrap process:
|
||||
// - already bootstrapped
|
||||
// - is unknown command
|
||||
// - is the default help command
|
||||
|
||||
@@ -44,6 +44,7 @@ func (t *TestApp) Cleanup() {
|
||||
}
|
||||
}
|
||||
|
||||
// NewMailClient initializes test app mail client.
|
||||
func (t *TestApp) NewMailClient() mailer.Mailer {
|
||||
t.mux.Lock()
|
||||
defer t.mux.Unlock()
|
||||
@@ -104,6 +105,14 @@ func NewTestApp(optTestDataDir ...string) (*TestApp, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure that the Dao and DB configurations are properly loaded
|
||||
if _, err := app.Dao().DB().NewQuery("Select 1").Execute(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := app.LogsDao().DB().NewQuery("Select 1").Execute(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// force disable request logs because the logs db call execute in a separate
|
||||
// go routine and it is possible to panic due to earlier api test completion.
|
||||
app.Settings().Logs.MaxDays = 0
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,87 @@
|
||||
package archive
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Create creates a new zip archive from src dir content and saves it in dest path.
|
||||
//
|
||||
// You can specify skipPaths to skip/ignore certain directories and files (relative to src)
|
||||
// preventing adding them in the final archive.
|
||||
func Create(src string, dest string, skipPaths ...string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zf, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer zf.Close()
|
||||
|
||||
zw := zip.NewWriter(zf)
|
||||
defer zw.Close()
|
||||
|
||||
if err := zipAddFS(zw, os.DirFS(src), skipPaths...); err != nil {
|
||||
// try to cleanup at least the created zip file
|
||||
os.Remove(dest)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// note remove after similar method is added in the std lib (https://github.com/golang/go/issues/54898)
|
||||
func zipAddFS(w *zip.Writer, fsys fs.FS, skipPaths ...string) error {
|
||||
return fs.WalkDir(fsys, ".", func(name string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// skip
|
||||
for _, ignore := range skipPaths {
|
||||
if ignore == name ||
|
||||
strings.HasPrefix(filepath.Clean(name)+string(os.PathSeparator), filepath.Clean(ignore)+string(os.PathSeparator)) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.Name = name
|
||||
h.Method = zip.Deflate
|
||||
|
||||
fw, err := w.CreateHeader(h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := fsys.Open(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = io.Copy(fw, f)
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package archive_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/archive"
|
||||
)
|
||||
|
||||
func TestCreateFailure(t *testing.T) {
|
||||
testDir := createTestDir(t)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
zipPath := filepath.Join(os.TempDir(), "pb_test.zip")
|
||||
defer os.RemoveAll(zipPath)
|
||||
|
||||
missingDir := filepath.Join(os.TempDir(), "missing")
|
||||
|
||||
if err := archive.Create(missingDir, zipPath); err == nil {
|
||||
t.Fatal("Expected to fail due to missing directory or file")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(zipPath); err == nil {
|
||||
t.Fatalf("Expected the zip file not to be created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSuccess(t *testing.T) {
|
||||
testDir := createTestDir(t)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
zipName := "pb_test.zip"
|
||||
zipPath := filepath.Join(os.TempDir(), zipName)
|
||||
defer os.RemoveAll(zipPath)
|
||||
|
||||
// zip testDir content (excluding test and a/b/c dir)
|
||||
if err := archive.Create(testDir, zipPath, "a/b/c", "test"); err != nil {
|
||||
t.Fatalf("Failed to create archive: %v", err)
|
||||
}
|
||||
|
||||
info, err := os.Stat(zipPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve the generated zip file: %v", err)
|
||||
}
|
||||
|
||||
if name := info.Name(); name != zipName {
|
||||
t.Fatalf("Expected zip with name %q, got %q", zipName, name)
|
||||
}
|
||||
|
||||
expectedSize := int64(405)
|
||||
if size := info.Size(); size != expectedSize {
|
||||
t.Fatalf("Expected zip with size %d, got %d", expectedSize, size)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// note: make sure to call os.RemoveAll(dir) after you are done
|
||||
// working with the created test dir.
|
||||
func createTestDir(t *testing.T) string {
|
||||
dir, err := os.MkdirTemp(os.TempDir(), "pb_zip_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(dir, "a/b/c"), os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
{
|
||||
f, err := os.OpenFile(filepath.Join(dir, "test"), os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
{
|
||||
f, err := os.OpenFile(filepath.Join(dir, "test2"), os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
{
|
||||
f, err := os.OpenFile(filepath.Join(dir, "a/test"), os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
{
|
||||
f, err := os.OpenFile(filepath.Join(dir, "a/b/sub1"), os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
{
|
||||
f, err := os.OpenFile(filepath.Join(dir, "a/b/c/sub2"), os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
{
|
||||
f, err := os.OpenFile(filepath.Join(dir, "a/b/c/sub3"), os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
return dir
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package archive
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Extract extracts the zip archive at src to dest.
|
||||
func Extract(src, dest string) error {
|
||||
zr, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer zr.Close()
|
||||
|
||||
// normalize dest path to check later for Zip Slip
|
||||
dest = filepath.Clean(dest) + string(os.PathSeparator)
|
||||
|
||||
for _, f := range zr.File {
|
||||
err := extractFile(f, dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractFile extracts the provided zipFile into "basePath/zipFileName" path,
|
||||
// creating all the necessary path directories.
|
||||
func extractFile(zipFile *zip.File, basePath string) error {
|
||||
path := filepath.Join(basePath, zipFile.Name)
|
||||
|
||||
// check for Zip Slip
|
||||
if !strings.HasPrefix(path, basePath) {
|
||||
return fmt.Errorf("invalid file path: %s", path)
|
||||
}
|
||||
|
||||
r, err := zipFile.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
if zipFile.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(path, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// ensure that the file path directories are created
|
||||
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zipFile.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = io.Copy(f, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package archive_test
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/archive"
|
||||
)
|
||||
|
||||
func TestExtractFailure(t *testing.T) {
|
||||
testDir := createTestDir(t)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
missingZipPath := filepath.Join(os.TempDir(), "pb_missing_test.zip")
|
||||
extractedPath := filepath.Join(os.TempDir(), "pb_zip_extract")
|
||||
defer os.RemoveAll(extractedPath)
|
||||
|
||||
if err := archive.Extract(missingZipPath, extractedPath); err == nil {
|
||||
t.Fatal("Expected Extract to fail due to missing zipPath")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(extractedPath); err == nil {
|
||||
t.Fatalf("Expected %q to not be created", extractedPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractSuccess(t *testing.T) {
|
||||
testDir := createTestDir(t)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
zipPath := filepath.Join(os.TempDir(), "pb_test.zip")
|
||||
defer os.RemoveAll(zipPath)
|
||||
|
||||
extractedPath := filepath.Join(os.TempDir(), "pb_zip_extract")
|
||||
defer os.RemoveAll(extractedPath)
|
||||
|
||||
// zip testDir content (with exclude)
|
||||
if err := archive.Create(testDir, zipPath, "a/b/c", "test", "sub2"); err != nil {
|
||||
t.Fatalf("Failed to create archive: %v", err)
|
||||
}
|
||||
|
||||
if err := archive.Extract(zipPath, extractedPath); err != nil {
|
||||
t.Fatalf("Failed to extract %q in %q", zipPath, extractedPath)
|
||||
}
|
||||
|
||||
availableFiles := []string{}
|
||||
|
||||
walkErr := filepath.WalkDir(extractedPath, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
availableFiles = append(availableFiles, path)
|
||||
|
||||
return nil
|
||||
})
|
||||
if walkErr != nil {
|
||||
t.Fatalf("Failed to read the extracted dir: %v", walkErr)
|
||||
}
|
||||
|
||||
expectedFiles := []string{
|
||||
filepath.Join(extractedPath, "test2"),
|
||||
filepath.Join(extractedPath, "a/test"),
|
||||
filepath.Join(extractedPath, "a/b/sub1"),
|
||||
}
|
||||
|
||||
if len(availableFiles) != len(expectedFiles) {
|
||||
t.Fatalf("Expected \n%v, \ngot \n%v", expectedFiles, availableFiles)
|
||||
}
|
||||
|
||||
ExpectedLoop:
|
||||
for _, expected := range expectedFiles {
|
||||
for _, available := range availableFiles {
|
||||
if available == expected {
|
||||
continue ExpectedLoop
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("Missing file %q in \n%v", expected, availableFiles)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Package cron implements a crontab-like service to execute and schedule
|
||||
// repeative tasks/jobs.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// c := cron.New()
|
||||
// c.MustAdd("dailyReport", "0 0 * * *", func() { ... })
|
||||
// c.Start()
|
||||
package cron
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type job struct {
|
||||
schedule *Schedule
|
||||
run func()
|
||||
}
|
||||
|
||||
// Cron is a crontab-like struct for tasks/jobs scheduling.
|
||||
type Cron struct {
|
||||
sync.RWMutex
|
||||
|
||||
interval time.Duration
|
||||
timezone *time.Location
|
||||
ticker *time.Ticker
|
||||
jobs map[string]*job
|
||||
}
|
||||
|
||||
// New create a new Cron struct with default tick interval of 1 minute
|
||||
// and timezone in UTC.
|
||||
//
|
||||
// You can change the default tick interval with Cron.SetInterval().
|
||||
// You can change the default timezone with Cron.SetTimezone().
|
||||
func New() *Cron {
|
||||
return &Cron{
|
||||
interval: 1 * time.Minute,
|
||||
timezone: time.UTC,
|
||||
jobs: map[string]*job{},
|
||||
}
|
||||
}
|
||||
|
||||
// SetInterval changes the current cron tick interval
|
||||
// (it usually should be >= 1 minute).
|
||||
func (c *Cron) SetInterval(d time.Duration) {
|
||||
// update interval
|
||||
c.Lock()
|
||||
wasStarted := c.ticker != nil
|
||||
c.interval = d
|
||||
c.Unlock()
|
||||
|
||||
// restart the ticker
|
||||
if wasStarted {
|
||||
c.Start()
|
||||
}
|
||||
}
|
||||
|
||||
// SetTimezone changes the current cron tick timezone.
|
||||
func (c *Cron) SetTimezone(l *time.Location) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
c.timezone = l
|
||||
}
|
||||
|
||||
// MustAdd is similar to Add() but panic on failure.
|
||||
func (c *Cron) MustAdd(jobId string, cronExpr string, run func()) {
|
||||
if err := c.Add(jobId, cronExpr, run); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add registers a single cron job.
|
||||
//
|
||||
// If there is already a job with the provided id, then the old job
|
||||
// will be replaced with the new one.
|
||||
//
|
||||
// cronExpr is a regular cron expression, eg. "0 */3 * * *" (aka. at minute 0 past every 3rd hour).
|
||||
// Check cron.NewSchedule() for the supported tokens.
|
||||
func (c *Cron) Add(jobId string, cronExpr string, run func()) error {
|
||||
if run == nil {
|
||||
return errors.New("failed to add new cron job: run must be non-nil function")
|
||||
}
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
schedule, err := NewSchedule(cronExpr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add new cron job: %w", err)
|
||||
}
|
||||
|
||||
c.jobs[jobId] = &job{
|
||||
schedule: schedule,
|
||||
run: run,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes a single cron job by its id.
|
||||
func (c *Cron) Remove(jobId string) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
delete(c.jobs, jobId)
|
||||
}
|
||||
|
||||
// RemoveAll removes all registered cron jobs.
|
||||
func (c *Cron) RemoveAll() {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
c.jobs = map[string]*job{}
|
||||
}
|
||||
|
||||
// Stop stops the current cron ticker (if not already).
|
||||
//
|
||||
// You can resume the ticker by calling Start().
|
||||
func (c *Cron) Stop() {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
if c.ticker == nil {
|
||||
return // already stopped
|
||||
}
|
||||
|
||||
c.ticker.Stop()
|
||||
c.ticker = nil
|
||||
}
|
||||
|
||||
// Start starts the cron ticker.
|
||||
//
|
||||
// Calling Start() on already started cron will restart the ticker.
|
||||
func (c *Cron) Start() {
|
||||
c.Stop()
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
c.ticker = time.NewTicker(c.interval)
|
||||
|
||||
go func() {
|
||||
for t := range c.ticker.C {
|
||||
c.runDue(t)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// HasStarted checks whether the current Cron ticker has been started.
|
||||
func (c *Cron) HasStarted() bool {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
||||
return c.ticker != nil
|
||||
}
|
||||
|
||||
// runDue runs all registered jobs that are scheduled for the provided time.
|
||||
func (c *Cron) runDue(t time.Time) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
||||
moment := NewMoment(t.In(c.timezone))
|
||||
|
||||
for _, j := range c.jobs {
|
||||
if j.schedule.IsDue(moment) {
|
||||
go j.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCronNew(t *testing.T) {
|
||||
c := New()
|
||||
|
||||
expectedInterval := 1 * time.Minute
|
||||
if c.interval != expectedInterval {
|
||||
t.Fatalf("Expected default interval %v, got %v", expectedInterval, c.interval)
|
||||
}
|
||||
|
||||
expectedTimezone := time.UTC
|
||||
if c.timezone.String() != expectedTimezone.String() {
|
||||
t.Fatalf("Expected default timezone %v, got %v", expectedTimezone, c.timezone)
|
||||
}
|
||||
|
||||
if len(c.jobs) != 0 {
|
||||
t.Fatalf("Expected no jobs by default, got \n%v", c.jobs)
|
||||
}
|
||||
|
||||
if c.ticker != nil {
|
||||
t.Fatal("Expected the ticker NOT to be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCronSetInterval(t *testing.T) {
|
||||
c := New()
|
||||
|
||||
interval := 2 * time.Minute
|
||||
|
||||
c.SetInterval(interval)
|
||||
|
||||
if c.interval != interval {
|
||||
t.Fatalf("Expected interval %v, got %v", interval, c.interval)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCronSetTimezone(t *testing.T) {
|
||||
c := New()
|
||||
|
||||
timezone, _ := time.LoadLocation("Asia/Tokyo")
|
||||
|
||||
c.SetTimezone(timezone)
|
||||
|
||||
if c.timezone.String() != timezone.String() {
|
||||
t.Fatalf("Expected timezone %v, got %v", timezone, c.timezone)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCronAddAndRemove(t *testing.T) {
|
||||
c := New()
|
||||
|
||||
if err := c.Add("test0", "* * * * *", nil); err == nil {
|
||||
t.Fatal("Expected nil function error")
|
||||
}
|
||||
|
||||
if err := c.Add("test1", "invalid", func() {}); err == nil {
|
||||
t.Fatal("Expected invalid cron expression error")
|
||||
}
|
||||
|
||||
if err := c.Add("test2", "* * * * *", func() {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := c.Add("test3", "* * * * *", func() {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := c.Add("test4", "* * * * *", func() {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// overwrite test2
|
||||
if err := c.Add("test2", "1 2 3 4 5", func() {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := c.Add("test5", "1 2 3 4 5", func() {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// mock job deletion
|
||||
c.Remove("test4")
|
||||
|
||||
// try to remove non-existing (should be no-op)
|
||||
c.Remove("missing")
|
||||
|
||||
// check job keys
|
||||
{
|
||||
expectedKeys := []string{"test3", "test2", "test5"}
|
||||
|
||||
if v := len(c.jobs); v != len(expectedKeys) {
|
||||
t.Fatalf("Expected %d jobs, got %d", len(expectedKeys), v)
|
||||
}
|
||||
|
||||
for _, k := range expectedKeys {
|
||||
if c.jobs[k] == nil {
|
||||
t.Fatalf("Expected job with key %s, got nil", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check the jobs schedule
|
||||
{
|
||||
expectedSchedules := map[string]string{
|
||||
"test2": `{"minutes":{"1":{}},"hours":{"2":{}},"days":{"3":{}},"months":{"4":{}},"daysOfWeek":{"5":{}}}`,
|
||||
"test3": `{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`,
|
||||
"test5": `{"minutes":{"1":{}},"hours":{"2":{}},"days":{"3":{}},"months":{"4":{}},"daysOfWeek":{"5":{}}}`,
|
||||
}
|
||||
for k, v := range expectedSchedules {
|
||||
raw, err := json.Marshal(c.jobs[k].schedule)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(raw) != v {
|
||||
t.Fatalf("Expected %q schedule \n%s, \ngot \n%s", k, v, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCronMustAdd(t *testing.T) {
|
||||
c := New()
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("test1 didn't panic")
|
||||
}
|
||||
}()
|
||||
|
||||
c.MustAdd("test1", "* * * * *", nil)
|
||||
|
||||
c.MustAdd("test2", "* * * * *", func() {})
|
||||
|
||||
if _, ok := c.jobs["test2"]; !ok {
|
||||
t.Fatal("Couldn't find job test2")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCronRemoveAll(t *testing.T) {
|
||||
c := New()
|
||||
|
||||
if err := c.Add("test1", "* * * * *", func() {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := c.Add("test2", "* * * * *", func() {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := c.Add("test3", "* * * * *", func() {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if v := len(c.jobs); v != 3 {
|
||||
t.Fatalf("Expected %d jobs, got %d", 3, v)
|
||||
}
|
||||
|
||||
c.RemoveAll()
|
||||
|
||||
if v := len(c.jobs); v != 0 {
|
||||
t.Fatalf("Expected %d jobs, got %d", 0, v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCronStartStop(t *testing.T) {
|
||||
c := New()
|
||||
|
||||
c.SetInterval(1 * time.Second)
|
||||
|
||||
test1 := 0
|
||||
test2 := 0
|
||||
|
||||
c.Add("test1", "* * * * *", func() {
|
||||
test1++
|
||||
})
|
||||
|
||||
c.Add("test2", "* * * * *", func() {
|
||||
test2++
|
||||
})
|
||||
|
||||
expectedCalls := 3
|
||||
|
||||
// call twice Start to check if the previous ticker will be reseted
|
||||
c.Start()
|
||||
c.Start()
|
||||
|
||||
time.Sleep(3250 * time.Millisecond)
|
||||
|
||||
// call twice Stop to ensure that the second stop is no-op
|
||||
c.Stop()
|
||||
c.Stop()
|
||||
|
||||
if test1 != expectedCalls {
|
||||
t.Fatalf("Expected %d test1, got %d", expectedCalls, test1)
|
||||
}
|
||||
if test2 != expectedCalls {
|
||||
t.Fatalf("Expected %d test2, got %d", expectedCalls, test2)
|
||||
}
|
||||
|
||||
// resume for ~5 seconds
|
||||
c.Start()
|
||||
time.Sleep(5250 * time.Millisecond)
|
||||
c.Stop()
|
||||
|
||||
expectedCalls += 5
|
||||
|
||||
if test1 != expectedCalls {
|
||||
t.Fatalf("Expected %d test1, got %d", expectedCalls, test1)
|
||||
}
|
||||
if test2 != expectedCalls {
|
||||
t.Fatalf("Expected %d test2, got %d", expectedCalls, test2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Moment represents a parsed single time moment.
|
||||
type Moment struct {
|
||||
Minute int `json:"minute"`
|
||||
Hour int `json:"hour"`
|
||||
Day int `json:"day"`
|
||||
Month int `json:"month"`
|
||||
DayOfWeek int `json:"dayOfWeek"`
|
||||
}
|
||||
|
||||
// NewMoment creates a new Moment from the specified time.
|
||||
func NewMoment(t time.Time) *Moment {
|
||||
return &Moment{
|
||||
Minute: t.Minute(),
|
||||
Hour: t.Hour(),
|
||||
Day: t.Day(),
|
||||
Month: int(t.Month()),
|
||||
DayOfWeek: int(t.Weekday()),
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule stores parsed information for each time component when a cron job should run.
|
||||
type Schedule struct {
|
||||
Minutes map[int]struct{} `json:"minutes"`
|
||||
Hours map[int]struct{} `json:"hours"`
|
||||
Days map[int]struct{} `json:"days"`
|
||||
Months map[int]struct{} `json:"months"`
|
||||
DaysOfWeek map[int]struct{} `json:"daysOfWeek"`
|
||||
}
|
||||
|
||||
// IsDue checks whether the provided Moment satisfies the current Schedule.
|
||||
func (s *Schedule) IsDue(m *Moment) bool {
|
||||
if _, ok := s.Minutes[m.Minute]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, ok := s.Hours[m.Hour]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, ok := s.Days[m.Day]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, ok := s.DaysOfWeek[m.DayOfWeek]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, ok := s.Months[m.Month]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// NewSchedule creates a new Schedule from a cron expression.
|
||||
//
|
||||
// A cron expression is consisted of 5 segments separated by space,
|
||||
// representing: minute, hour, day of the month, month and day of the week.
|
||||
//
|
||||
// Each segment could be in the following formats:
|
||||
// - wildcard: *
|
||||
// - range: 1-30
|
||||
// - step: */n or 1-30/n
|
||||
// - list: 1,2,3,10-20/n
|
||||
func NewSchedule(cronExpr string) (*Schedule, error) {
|
||||
segments := strings.Split(cronExpr, " ")
|
||||
if len(segments) != 5 {
|
||||
return nil, errors.New("invalid cron expression - must have exactly 5 space separated segments")
|
||||
}
|
||||
|
||||
minutes, err := parseCronSegment(segments[0], 0, 59)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hours, err := parseCronSegment(segments[1], 0, 23)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
days, err := parseCronSegment(segments[2], 1, 31)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
months, err := parseCronSegment(segments[3], 1, 12)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
daysOfWeek, err := parseCronSegment(segments[4], 0, 6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Schedule{
|
||||
Minutes: minutes,
|
||||
Hours: hours,
|
||||
Days: days,
|
||||
Months: months,
|
||||
DaysOfWeek: daysOfWeek,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseCronSegment parses a single cron expression segment and
|
||||
// returns its time schedule slots.
|
||||
func parseCronSegment(segment string, min int, max int) (map[int]struct{}, error) {
|
||||
slots := map[int]struct{}{}
|
||||
|
||||
list := strings.Split(segment, ",")
|
||||
for _, p := range list {
|
||||
stepParts := strings.Split(p, "/")
|
||||
|
||||
// step (*/n, 1-30/n)
|
||||
var step int
|
||||
switch len(stepParts) {
|
||||
case 1:
|
||||
step = 1
|
||||
case 2:
|
||||
parsedStep, err := strconv.Atoi(stepParts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parsedStep < 1 || parsedStep > max {
|
||||
return nil, fmt.Errorf("invalid segment step boundary - the step must be between 1 and the %d", max)
|
||||
}
|
||||
step = parsedStep
|
||||
default:
|
||||
return nil, errors.New("invalid segment step format - must be in the format */n or 1-30/n")
|
||||
}
|
||||
|
||||
// find the min and max range of the segment part
|
||||
var rangeMin, rangeMax int
|
||||
if stepParts[0] == "*" {
|
||||
rangeMin = min
|
||||
rangeMax = max
|
||||
} else {
|
||||
// single digit (1) or range (1-30)
|
||||
rangeParts := strings.Split(stepParts[0], "-")
|
||||
switch len(rangeParts) {
|
||||
case 1:
|
||||
if step != 1 {
|
||||
return nil, errors.New("invalid segement step - step > 1 could be used only with the wildcard or range format")
|
||||
}
|
||||
parsed, err := strconv.Atoi(rangeParts[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parsed < min || parsed > max {
|
||||
return nil, errors.New("invalid segment value - must be between the min and max of the segment")
|
||||
}
|
||||
rangeMin = parsed
|
||||
rangeMax = rangeMin
|
||||
case 2:
|
||||
parsedMin, err := strconv.Atoi(rangeParts[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parsedMin < min || parsedMin > max {
|
||||
return nil, fmt.Errorf("invalid segment range minimum - must be between %d and %d", min, max)
|
||||
}
|
||||
rangeMin = parsedMin
|
||||
|
||||
parsedMax, err := strconv.Atoi(rangeParts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parsedMax < parsedMin || parsedMax > max {
|
||||
return nil, fmt.Errorf("invalid segment range maximum - must be between %d and %d", rangeMin, max)
|
||||
}
|
||||
rangeMax = parsedMax
|
||||
default:
|
||||
return nil, errors.New("invalid segment range format - the range must have 1 or 2 parts")
|
||||
}
|
||||
}
|
||||
|
||||
// fill the slots
|
||||
for i := rangeMin; i <= rangeMax; i += step {
|
||||
slots[i] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return slots, nil
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
package cron_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/cron"
|
||||
)
|
||||
|
||||
func TestNewMoment(t *testing.T) {
|
||||
date, err := time.Parse("2006-01-02 15:04", "2023-05-09 15:20")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
m := cron.NewMoment(date)
|
||||
|
||||
if m.Minute != 20 {
|
||||
t.Fatalf("Expected m.Minute %d, got %d", 20, m.Minute)
|
||||
}
|
||||
|
||||
if m.Hour != 15 {
|
||||
t.Fatalf("Expected m.Hour %d, got %d", 15, m.Hour)
|
||||
}
|
||||
|
||||
if m.Day != 9 {
|
||||
t.Fatalf("Expected m.Day %d, got %d", 9, m.Day)
|
||||
}
|
||||
|
||||
if m.Month != 5 {
|
||||
t.Fatalf("Expected m.Month %d, got %d", 5, m.Month)
|
||||
}
|
||||
|
||||
if m.DayOfWeek != 2 {
|
||||
t.Fatalf("Expected m.DayOfWeek %d, got %d", 2, m.DayOfWeek)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSchedule(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
cronExpr string
|
||||
expectError bool
|
||||
expectSchedule string
|
||||
}{
|
||||
{
|
||||
"invalid",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"* * * *",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"* * * * * *",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"2/3 * * * *",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"* * * * *",
|
||||
false,
|
||||
`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`,
|
||||
},
|
||||
{
|
||||
"*/2 */3 */5 */4 */2",
|
||||
false,
|
||||
`{"minutes":{"0":{},"10":{},"12":{},"14":{},"16":{},"18":{},"2":{},"20":{},"22":{},"24":{},"26":{},"28":{},"30":{},"32":{},"34":{},"36":{},"38":{},"4":{},"40":{},"42":{},"44":{},"46":{},"48":{},"50":{},"52":{},"54":{},"56":{},"58":{},"6":{},"8":{}},"hours":{"0":{},"12":{},"15":{},"18":{},"21":{},"3":{},"6":{},"9":{}},"days":{"1":{},"11":{},"16":{},"21":{},"26":{},"31":{},"6":{}},"months":{"1":{},"5":{},"9":{}},"daysOfWeek":{"0":{},"2":{},"4":{},"6":{}}}`,
|
||||
},
|
||||
|
||||
// minute segment
|
||||
{
|
||||
"-1 * * * *",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"60 * * * *",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"0 * * * *",
|
||||
false,
|
||||
`{"minutes":{"0":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`,
|
||||
},
|
||||
{
|
||||
"59 * * * *",
|
||||
false,
|
||||
`{"minutes":{"59":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`,
|
||||
},
|
||||
{
|
||||
"1,2,5,7,40-50/2 * * * *",
|
||||
false,
|
||||
`{"minutes":{"1":{},"2":{},"40":{},"42":{},"44":{},"46":{},"48":{},"5":{},"50":{},"7":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`,
|
||||
},
|
||||
|
||||
// hour segment
|
||||
{
|
||||
"* -1 * * *",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"* 24 * * *",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"* 0 * * *",
|
||||
false,
|
||||
`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`,
|
||||
},
|
||||
{
|
||||
"* 23 * * *",
|
||||
false,
|
||||
`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"23":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`,
|
||||
},
|
||||
{
|
||||
"* 3,4,8-16/3,7 * * *",
|
||||
false,
|
||||
`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"11":{},"14":{},"3":{},"4":{},"7":{},"8":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`,
|
||||
},
|
||||
|
||||
// day segment
|
||||
{
|
||||
"* * 0 * *",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"* * 32 * *",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"* * 1 * *",
|
||||
false,
|
||||
`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`,
|
||||
},
|
||||
{
|
||||
"* * 31 * *",
|
||||
false,
|
||||
`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"31":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`,
|
||||
},
|
||||
{
|
||||
"* * 5,6,20-30/3,1 * *",
|
||||
false,
|
||||
`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"20":{},"23":{},"26":{},"29":{},"5":{},"6":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`,
|
||||
},
|
||||
|
||||
// month segment
|
||||
{
|
||||
"* * * 0 *",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"* * * 13 *",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"* * * 1 *",
|
||||
false,
|
||||
`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`,
|
||||
},
|
||||
{
|
||||
"* * * 12 *",
|
||||
false,
|
||||
`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"12":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`,
|
||||
},
|
||||
{
|
||||
"* * * 1,4,5-10/2 *",
|
||||
false,
|
||||
`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"4":{},"5":{},"7":{},"9":{}},"daysOfWeek":{"0":{},"1":{},"2":{},"3":{},"4":{},"5":{},"6":{}}}`,
|
||||
},
|
||||
|
||||
// day of week segment
|
||||
{
|
||||
"* * * * -1",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"* * * * 7",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"* * * * 0",
|
||||
false,
|
||||
`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"0":{}}}`,
|
||||
},
|
||||
{
|
||||
"* * * * 6",
|
||||
false,
|
||||
`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"6":{}}}`,
|
||||
},
|
||||
{
|
||||
"* * * * 1,2-5/2",
|
||||
false,
|
||||
`{"minutes":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"32":{},"33":{},"34":{},"35":{},"36":{},"37":{},"38":{},"39":{},"4":{},"40":{},"41":{},"42":{},"43":{},"44":{},"45":{},"46":{},"47":{},"48":{},"49":{},"5":{},"50":{},"51":{},"52":{},"53":{},"54":{},"55":{},"56":{},"57":{},"58":{},"59":{},"6":{},"7":{},"8":{},"9":{}},"hours":{"0":{},"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"days":{"1":{},"10":{},"11":{},"12":{},"13":{},"14":{},"15":{},"16":{},"17":{},"18":{},"19":{},"2":{},"20":{},"21":{},"22":{},"23":{},"24":{},"25":{},"26":{},"27":{},"28":{},"29":{},"3":{},"30":{},"31":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"months":{"1":{},"10":{},"11":{},"12":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{},"8":{},"9":{}},"daysOfWeek":{"1":{},"2":{},"4":{}}}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
schedule, err := cron.NewSchedule(s.cronExpr)
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Fatalf("[%s] Expected hasErr to be %v, got %v (%v)", s.cronExpr, s.expectError, hasErr, err)
|
||||
}
|
||||
|
||||
if hasErr {
|
||||
continue
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(schedule)
|
||||
if err != nil {
|
||||
t.Fatalf("[%s] Failed to marshalize the result schedule: %v", s.cronExpr, err)
|
||||
}
|
||||
encodedStr := string(encoded)
|
||||
|
||||
if encodedStr != s.expectSchedule {
|
||||
t.Fatalf("[%s] Expected \n%s, \ngot \n%s", s.cronExpr, s.expectSchedule, encodedStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduleIsDue(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
cronExpr string
|
||||
moment *cron.Moment
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
"* * * * *",
|
||||
&cron.Moment{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"* * * * *",
|
||||
&cron.Moment{
|
||||
Minute: 1,
|
||||
Hour: 1,
|
||||
Day: 1,
|
||||
Month: 1,
|
||||
DayOfWeek: 1,
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"5 * * * *",
|
||||
&cron.Moment{
|
||||
Minute: 1,
|
||||
Hour: 1,
|
||||
Day: 1,
|
||||
Month: 1,
|
||||
DayOfWeek: 1,
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"5 * * * *",
|
||||
&cron.Moment{
|
||||
Minute: 5,
|
||||
Hour: 1,
|
||||
Day: 1,
|
||||
Month: 1,
|
||||
DayOfWeek: 1,
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"* 2-6 * * 2,3",
|
||||
&cron.Moment{
|
||||
Minute: 1,
|
||||
Hour: 2,
|
||||
Day: 1,
|
||||
Month: 1,
|
||||
DayOfWeek: 1,
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"* 2-6 * * 2,3",
|
||||
&cron.Moment{
|
||||
Minute: 1,
|
||||
Hour: 2,
|
||||
Day: 1,
|
||||
Month: 1,
|
||||
DayOfWeek: 3,
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"* * 1,2,5,15-18 * *",
|
||||
&cron.Moment{
|
||||
Minute: 1,
|
||||
Hour: 1,
|
||||
Day: 6,
|
||||
Month: 1,
|
||||
DayOfWeek: 1,
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"* * 1,2,5,15-18/2 * *",
|
||||
&cron.Moment{
|
||||
Minute: 1,
|
||||
Hour: 1,
|
||||
Day: 2,
|
||||
Month: 1,
|
||||
DayOfWeek: 1,
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"* * 1,2,5,15-18/2 * *",
|
||||
&cron.Moment{
|
||||
Minute: 1,
|
||||
Hour: 1,
|
||||
Day: 18,
|
||||
Month: 1,
|
||||
DayOfWeek: 1,
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"* * 1,2,5,15-18/2 * *",
|
||||
&cron.Moment{
|
||||
Minute: 1,
|
||||
Hour: 1,
|
||||
Day: 17,
|
||||
Month: 1,
|
||||
DayOfWeek: 1,
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
schedule, err := cron.NewSchedule(s.cronExpr)
|
||||
if err != nil {
|
||||
t.Fatalf("[%d-%s] Unexpected cron error: %v", i, s.cronExpr, err)
|
||||
}
|
||||
|
||||
result := schedule.IsDue(s.moment)
|
||||
|
||||
if result != s.expected {
|
||||
t.Fatalf("[%d-%s] Expected %v, got %v", i, s.cronExpr, s.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ 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]*))?`)
|
||||
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))?$`)
|
||||
)
|
||||
|
||||
|
||||
@@ -18,9 +18,20 @@ func TestParseIndex(t *testing.T) {
|
||||
`invalid`,
|
||||
dbutils.Index{},
|
||||
},
|
||||
// simple
|
||||
// simple (multiple spaces between the table and columns list)
|
||||
{
|
||||
`create index indexname on tablename (col1)`,
|
||||
`create index indexname on tablename (col1)`,
|
||||
dbutils.Index{
|
||||
IndexName: "indexname",
|
||||
TableName: "tablename",
|
||||
Columns: []dbutils.IndexColumn{
|
||||
{Name: "col1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
// simple (no space between the table and the columns list)
|
||||
{
|
||||
`create index indexname on tablename(col1)`,
|
||||
dbutils.Index{
|
||||
IndexName: "indexname",
|
||||
TableName: "tablename",
|
||||
|
||||
@@ -82,6 +82,11 @@ func NewLocal(dirPath string) (*System, error) {
|
||||
return &System{ctx: ctx, bucket: bucket}, nil
|
||||
}
|
||||
|
||||
// SetContext assigns the specified context to the current filesystem.
|
||||
func (s *System) SetContext(ctx context.Context) {
|
||||
s.ctx = ctx
|
||||
}
|
||||
|
||||
// Close releases any resources used for the related filesystem.
|
||||
func (s *System) Close() error {
|
||||
return s.bucket.Close()
|
||||
@@ -109,6 +114,28 @@ func (s *System) GetFile(fileKey string) (*blob.Reader, error) {
|
||||
return br, nil
|
||||
}
|
||||
|
||||
// List returns a flat list with info for all files under the specified prefix.
|
||||
func (s *System) List(prefix string) ([]*blob.ListObject, error) {
|
||||
files := []*blob.ListObject{}
|
||||
|
||||
iter := s.bucket.List(&blob.ListOptions{
|
||||
Prefix: prefix,
|
||||
})
|
||||
|
||||
for {
|
||||
obj, err := iter.Next(s.ctx)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
break
|
||||
}
|
||||
files = append(files, obj)
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// Upload writes content into the fileKey location.
|
||||
func (s *System) Upload(content []byte, fileKey string) error {
|
||||
opts := &blob.WriterOptions{
|
||||
|
||||
@@ -401,6 +401,67 @@ func TestFileSystemGetFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSystemList(t *testing.T) {
|
||||
dir := createTestDir(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
fs, err := filesystem.NewLocal(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer fs.Close()
|
||||
|
||||
scenarios := []struct {
|
||||
prefix string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
"",
|
||||
[]string{
|
||||
"image.png",
|
||||
"image.svg",
|
||||
"image_! noext",
|
||||
"style.css",
|
||||
"test/sub1.txt",
|
||||
"test/sub2.txt",
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
[]string{
|
||||
"test/sub1.txt",
|
||||
"test/sub2.txt",
|
||||
},
|
||||
},
|
||||
{
|
||||
"missing",
|
||||
[]string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
objs, err := fs.List(s.prefix)
|
||||
if err != nil {
|
||||
t.Fatalf("[%s] %v", s.prefix, err)
|
||||
}
|
||||
|
||||
if len(s.expected) != len(objs) {
|
||||
t.Fatalf("[%s] Expected %d files, got \n%v", s.prefix, len(s.expected), objs)
|
||||
}
|
||||
|
||||
ObjsLoop:
|
||||
for _, obj := range objs {
|
||||
for _, name := range s.expected {
|
||||
if name == obj.Key {
|
||||
continue ObjsLoop
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("[%s] Unexpected file %q", s.prefix, obj.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSystemServeSingleRange(t *testing.T) {
|
||||
dir := createTestDir(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
@@ -37,6 +37,8 @@ func (h *Hook[T]) Add(fn Handler[T]) {
|
||||
}
|
||||
|
||||
// Reset removes all registered handlers.
|
||||
//
|
||||
// @todo for consistency with other Go methods consider renaming it to Clear.
|
||||
func (h *Hook[T]) Reset() {
|
||||
h.mux.Lock()
|
||||
defer h.mux.Unlock()
|
||||
|
||||
@@ -27,9 +27,6 @@ func SubtractSlice[T comparable](base []T, subtract []T) []T {
|
||||
|
||||
// ExistInSlice checks whether a comparable element exists in a slice of the same type.
|
||||
func ExistInSlice[T comparable](item T, list []T) bool {
|
||||
if len(list) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, v := range list {
|
||||
if v == item {
|
||||
|
||||
@@ -50,7 +50,6 @@ func (r *Runner) Run(args ...string) error {
|
||||
case "up":
|
||||
applied, err := r.Up()
|
||||
if err != nil {
|
||||
color.Red(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -75,7 +74,6 @@ func (r *Runner) Run(args ...string) error {
|
||||
|
||||
names, err := r.lastAppliedMigrations(toRevertCount)
|
||||
if err != nil {
|
||||
color.Red(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -95,7 +93,6 @@ func (r *Runner) Run(args ...string) error {
|
||||
|
||||
reverted, err := r.Down(toRevertCount)
|
||||
if err != nil {
|
||||
color.Red(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -110,7 +107,6 @@ func (r *Runner) Run(args ...string) error {
|
||||
return nil
|
||||
case "history-sync":
|
||||
if err := r.removeMissingAppliedMigrations(); err != nil {
|
||||
color.Red(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package osutils
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
)
|
||||
|
||||
// MoveDirContent moves the src dir content, that is not listed in the exclide list,
|
||||
// to dest dir (it will be created if missing).
|
||||
//
|
||||
// The rootExclude argument is used to specify a list of src root entries to exclude.
|
||||
//
|
||||
// Note that this method doesn't delete the old src dir.
|
||||
//
|
||||
// It is an alternative to os.Rename() for the cases where we can't
|
||||
// rename/delete the src dir (see https://github.com/pocketbase/pocketbase/issues/2519).
|
||||
func MoveDirContent(src string, dest string, rootExclude ...string) error {
|
||||
entries, err := os.ReadDir(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// make sure that the dest dir exist
|
||||
manuallyCreatedDestDir := false
|
||||
if _, err := os.Stat(dest); err != nil {
|
||||
if err := os.Mkdir(dest, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
manuallyCreatedDestDir = true
|
||||
}
|
||||
|
||||
moved := map[string]string{}
|
||||
|
||||
tryRollback := func() ([]error) {
|
||||
errs := []error{}
|
||||
|
||||
for old, new := range moved {
|
||||
if err := os.Rename(new, old); err != nil{
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// try to delete manually the created dest dir if all moved files were restored
|
||||
if manuallyCreatedDestDir && len(errs) == 0 {
|
||||
if err := os.Remove(dest); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
basename := entry.Name()
|
||||
|
||||
if list.ExistInSlice(basename, rootExclude) {
|
||||
continue
|
||||
}
|
||||
|
||||
old := filepath.Join(src, basename)
|
||||
new := filepath.Join(dest, basename)
|
||||
|
||||
if err := os.Rename(old, new); err != nil {
|
||||
if errs := tryRollback(); len(errs) > 0 {
|
||||
// currently just log the rollback errors
|
||||
// in the future we may require go 1.20+ to use errors.Join()
|
||||
log.Println(errs)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
moved[old] = new
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package osutils_test
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
"github.com/pocketbase/pocketbase/tools/osutils"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
func TestMoveDirContent(t *testing.T) {
|
||||
testDir := createTestDir(t)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
exclude := []string{
|
||||
"missing",
|
||||
"test2",
|
||||
"b",
|
||||
}
|
||||
|
||||
// missing dest path
|
||||
// ---
|
||||
dir1 := filepath.Join(filepath.Dir(testDir), "a", "b", "c", "d", "_pb_move_dir_content_test_" + security.PseudorandomString(4))
|
||||
defer os.RemoveAll(dir1)
|
||||
|
||||
if err := osutils.MoveDirContent(testDir, dir1, exclude...); err == nil {
|
||||
t.Fatal("Expected path error, got nil")
|
||||
}
|
||||
|
||||
// existing parent dir
|
||||
// ---
|
||||
dir2 := filepath.Join(filepath.Dir(testDir), "_pb_move_dir_content_test_" + security.PseudorandomString(4))
|
||||
defer os.RemoveAll(dir2)
|
||||
|
||||
if err := osutils.MoveDirContent(testDir, dir2, exclude...); err != nil {
|
||||
t.Fatalf("Expected dir2 to be created, got error: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// find all files
|
||||
files := []string{}
|
||||
filepath.WalkDir(dir2, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
files = append(files, path)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
expectedFiles := []string{
|
||||
filepath.Join(dir2, "test1"),
|
||||
filepath.Join(dir2, "a", "a1"),
|
||||
filepath.Join(dir2, "a", "a2"),
|
||||
};
|
||||
|
||||
if len(files) != len(expectedFiles) {
|
||||
t.Fatalf("Expected %d files, got %d: \n%v", len(expectedFiles), len(files), files)
|
||||
}
|
||||
|
||||
for _, expected := range expectedFiles {
|
||||
if !list.ExistInSlice(expected, files) {
|
||||
t.Fatalf("Missing expected file %q in \n%v", expected, files)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// note: make sure to call os.RemoveAll(dir) after you are done
|
||||
// working with the created test dir.
|
||||
func createTestDir(t *testing.T) string {
|
||||
dir, err := os.MkdirTemp(os.TempDir(), "test_dir")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// create sub directories
|
||||
if err := os.MkdirAll(filepath.Join(dir, "a"), os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(dir, "b"), os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
f, err := os.OpenFile(filepath.Join(dir, "test1"), os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
{
|
||||
f, err := os.OpenFile(filepath.Join(dir, "test2"), os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
{
|
||||
f, err := os.OpenFile(filepath.Join(dir, "a/a1"), os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
{
|
||||
f, err := os.OpenFile(filepath.Join(dir, "a/a2"), os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
{
|
||||
f, err := os.OpenFile(filepath.Join(dir, "b/b2"), os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
{
|
||||
f, err := os.OpenFile(filepath.Join(dir, "b/b2"), os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
return dir
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase/tools/search"
|
||||
)
|
||||
|
||||
// Serializer represents custom REST JSON serializer based on echo.DefaultJSONSerializer,
|
||||
// with support for additional generic response data transformation (eg. fields picker).
|
||||
type Serializer struct {
|
||||
echo.DefaultJSONSerializer
|
||||
|
||||
FieldsParam string
|
||||
}
|
||||
|
||||
// Serialize converts an interface into a json and writes it to the response.
|
||||
//
|
||||
// It also provides a generic response data fields picker via the FieldsParam query parameter (default to "fields").
|
||||
func (s *Serializer) Serialize(c echo.Context, i any, indent string) error {
|
||||
fieldsParam := s.FieldsParam
|
||||
if fieldsParam == "" {
|
||||
fieldsParam = "fields"
|
||||
}
|
||||
|
||||
param := c.QueryParam(fieldsParam)
|
||||
if param == "" {
|
||||
return s.DefaultJSONSerializer.Serialize(c, i, indent)
|
||||
}
|
||||
|
||||
fields := strings.Split(param, ",")
|
||||
for i, f := range fields {
|
||||
fields[i] = strings.TrimSpace(f)
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var decoded any
|
||||
|
||||
if err := json.Unmarshal(encoded, &decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var isSearchResult bool
|
||||
|
||||
switch i.(type) {
|
||||
case search.Result, *search.Result:
|
||||
isSearchResult = true
|
||||
}
|
||||
|
||||
if isSearchResult {
|
||||
if decodedMap, ok := decoded.(map[string]any); ok {
|
||||
pickFields(decodedMap["items"], fields)
|
||||
}
|
||||
} else {
|
||||
pickFields(decoded, fields)
|
||||
}
|
||||
|
||||
return s.DefaultJSONSerializer.Serialize(c, decoded, indent)
|
||||
}
|
||||
|
||||
func pickFields(data any, fields []string) {
|
||||
switch v := data.(type) {
|
||||
case map[string]any:
|
||||
pickMapFields(v, fields)
|
||||
case []map[string]any:
|
||||
for _, item := range v {
|
||||
pickMapFields(item, fields)
|
||||
}
|
||||
case []any:
|
||||
if len(v) == 0 {
|
||||
return // nothing to pick
|
||||
}
|
||||
|
||||
if _, ok := v[0].(map[string]any); !ok {
|
||||
return // for now ignore non-map values
|
||||
}
|
||||
|
||||
for _, item := range v {
|
||||
pickMapFields(item.(map[string]any), fields)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pickMapFields(data map[string]any, fields []string) {
|
||||
if len(fields) == 0 {
|
||||
return // nothing to pick
|
||||
}
|
||||
|
||||
DataLoop:
|
||||
for k := range data {
|
||||
matchingFields := make([]string, 0, len(fields))
|
||||
for _, f := range fields {
|
||||
if strings.HasPrefix(f+".", k+".") {
|
||||
matchingFields = append(matchingFields, f)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(matchingFields) == 0 {
|
||||
delete(data, k)
|
||||
continue DataLoop
|
||||
}
|
||||
|
||||
// trim the key from the fields
|
||||
for i, v := range matchingFields {
|
||||
trimmed := strings.TrimSuffix(strings.TrimPrefix(v+".", k+"."), ".")
|
||||
if trimmed == "" {
|
||||
continue DataLoop
|
||||
}
|
||||
matchingFields[i] = trimmed
|
||||
}
|
||||
|
||||
pickFields(data[k], matchingFields)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package rest_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase/tools/rest"
|
||||
"github.com/pocketbase/pocketbase/tools/search"
|
||||
)
|
||||
|
||||
func TestSerialize(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
serializer rest.Serializer
|
||||
data any
|
||||
query string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"empty query",
|
||||
rest.Serializer{},
|
||||
map[string]any{"a": 1, "b": 2, "c": "test"},
|
||||
"",
|
||||
`{"a":1,"b":2,"c":"test"}`,
|
||||
},
|
||||
{
|
||||
"empty fields",
|
||||
rest.Serializer{},
|
||||
map[string]any{"a": 1, "b": 2, "c": "test"},
|
||||
"fields=",
|
||||
`{"a":1,"b":2,"c":"test"}`,
|
||||
},
|
||||
{
|
||||
"missing fields",
|
||||
rest.Serializer{},
|
||||
map[string]any{"a": 1, "b": 2, "c": "test"},
|
||||
"fields=missing",
|
||||
`{}`,
|
||||
},
|
||||
{
|
||||
"non map response",
|
||||
rest.Serializer{},
|
||||
"test",
|
||||
"fields=a,b,test",
|
||||
`"test"`,
|
||||
},
|
||||
{
|
||||
"non slice of map response",
|
||||
rest.Serializer{},
|
||||
[]any{"a", "b", "test"},
|
||||
"fields=a,test",
|
||||
`["a","b","test"]`,
|
||||
},
|
||||
{
|
||||
"map with existing and missing fields",
|
||||
rest.Serializer{},
|
||||
map[string]any{"a": 1, "b": 2, "c": "test"},
|
||||
"fields=a, c ,missing", // test individual fields trim
|
||||
`{"a":1,"c":"test"}`,
|
||||
},
|
||||
{
|
||||
"custom fields param",
|
||||
rest.Serializer{FieldsParam: "custom"},
|
||||
map[string]any{"a": 1, "b": 2, "c": "test"},
|
||||
"custom=a, c ,missing", // test individual fields trim
|
||||
`{"a":1,"c":"test"}`,
|
||||
},
|
||||
{
|
||||
"slice of maps with existing and missing fields",
|
||||
rest.Serializer{},
|
||||
[]any{
|
||||
map[string]any{"a": 11, "b": 11, "c": "test1"},
|
||||
map[string]any{"a": 22, "b": 22, "c": "test2"},
|
||||
},
|
||||
"fields=a, c ,missing", // test individual fields trim
|
||||
`[{"a":11,"c":"test1"},{"a":22,"c":"test2"}]`,
|
||||
},
|
||||
{
|
||||
"nested fields with mixed map and any slices",
|
||||
rest.Serializer{},
|
||||
map[string]any{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": "test",
|
||||
"anySlice": []any{
|
||||
map[string]any{
|
||||
"A": []int{1, 2, 3},
|
||||
"B": []any{"1", "2", 3},
|
||||
"C": "test",
|
||||
"D": map[string]any{
|
||||
"DA": 1,
|
||||
"DB": 2,
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"A": "test",
|
||||
},
|
||||
},
|
||||
"mapSlice": []map[string]any{
|
||||
{
|
||||
"A": []int{1, 2, 3},
|
||||
"B": []any{"1", "2", 3},
|
||||
"C": "test",
|
||||
"D": []any{
|
||||
map[string]any{"DA": 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
"B": []any{"1", "2", 3},
|
||||
"D": []any{
|
||||
map[string]any{"DA": 2},
|
||||
map[string]any{"DA": 3},
|
||||
map[string]any{"DB": 4}, // will result to empty since there is no DA
|
||||
},
|
||||
},
|
||||
},
|
||||
"fullMap": []map[string]any{
|
||||
{
|
||||
"A": []int{1, 2, 3},
|
||||
"B": []any{"1", "2", 3},
|
||||
"C": "test",
|
||||
},
|
||||
{
|
||||
"B": []any{"1", "2", 3},
|
||||
"D": []any{
|
||||
map[string]any{"DA": 2},
|
||||
map[string]any{"DA": 3}, // will result to empty since there is no DA
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"fields=a, c, anySlice.A, mapSlice.C, mapSlice.D.DA, anySlice.D,fullMap",
|
||||
`{"a":1,"anySlice":[{"A":[1,2,3],"D":{"DA":1,"DB":2}},{"A":"test"}],"c":"test","fullMap":[{"A":[1,2,3],"B":["1","2",3],"C":"test"},{"B":["1","2",3],"D":[{"DA":2},{"DA":3}]}],"mapSlice":[{"C":"test","D":[{"DA":1}]},{"D":[{"DA":2},{"DA":3},{}]}]}`,
|
||||
},
|
||||
{
|
||||
"SearchResult",
|
||||
rest.Serializer{},
|
||||
search.Result{
|
||||
Page: 1,
|
||||
PerPage: 10,
|
||||
TotalItems: 20,
|
||||
TotalPages: 30,
|
||||
Items: []any{
|
||||
map[string]any{"a": 11, "b": 11, "c": "test1"},
|
||||
map[string]any{"a": 22, "b": 22, "c": "test2"},
|
||||
},
|
||||
},
|
||||
"fields=a,c,missing",
|
||||
`{"items":[{"a":11,"c":"test1"},{"a":22,"c":"test2"}],"page":1,"perPage":10,"totalItems":20,"totalPages":30}`,
|
||||
},
|
||||
{
|
||||
"*SearchResult",
|
||||
rest.Serializer{},
|
||||
&search.Result{
|
||||
Page: 1,
|
||||
PerPage: 10,
|
||||
TotalItems: 20,
|
||||
TotalPages: 30,
|
||||
Items: []any{
|
||||
map[string]any{"a": 11, "b": 11, "c": "test1"},
|
||||
map[string]any{"a": 22, "b": 22, "c": "test2"},
|
||||
},
|
||||
},
|
||||
"fields=a,c",
|
||||
`{"items":[{"a":11,"c":"test1"},{"a":22,"c":"test2"}],"page":1,"perPage":10,"totalItems":20,"totalPages":30}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
req.URL.RawQuery = s.query
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
|
||||
if err := s.serializer.Serialize(c, s.data, ""); err != nil {
|
||||
t.Errorf("[%s] Serialize failure: %v", s.name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
rawBody, err := io.ReadAll(rec.Result().Body)
|
||||
if err != nil {
|
||||
t.Errorf("[%s] Failed to read request body: %v", s.name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if v := strings.TrimSpace(string(rawBody)); v != s.expected {
|
||||
t.Fatalf("[%s] Expected body\n%v \ngot: \n%v", s.name, s.expected, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,11 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// BindBody binds request body content to i.
|
||||
@@ -28,10 +30,12 @@ func BindBody(c echo.Context, i interface{}) error {
|
||||
return echo.NewHTTPErrorWithInternal(http.StatusBadRequest, err, err.Error())
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
// fallback to the default binder
|
||||
return echo.BindBody(c, i)
|
||||
case strings.HasPrefix(ctype, echo.MIMEApplicationForm), strings.HasPrefix(ctype, echo.MIMEMultipartForm):
|
||||
return bindFormData(c, i)
|
||||
}
|
||||
|
||||
// fallback to the default binder
|
||||
return echo.BindBody(c, i)
|
||||
}
|
||||
|
||||
// CopyJsonBody reads the request body into i by
|
||||
@@ -57,3 +61,71 @@ func CopyJsonBody(r *http.Request, i interface{}) error {
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// This is temp hotfix for properly binding multipart/form-data array values
|
||||
// when a map destination is used.
|
||||
//
|
||||
// It should be replaced with echo.BindBody(c, i) once the issue is fixed in echo.
|
||||
func bindFormData(c echo.Context, i interface{}) error {
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
values, err := c.FormValues()
|
||||
if err != nil {
|
||||
return echo.NewHTTPErrorWithInternal(http.StatusBadRequest, err, err.Error())
|
||||
}
|
||||
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
rt := reflect.TypeOf(i).Elem()
|
||||
|
||||
// map
|
||||
if rt.Kind() == reflect.Map {
|
||||
rv := reflect.ValueOf(i).Elem()
|
||||
|
||||
for k, v := range values {
|
||||
if total := len(v); total == 1 {
|
||||
rv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(normalizeMultipartValue(v[0])))
|
||||
} else {
|
||||
normalized := make([]any, total)
|
||||
for i, vItem := range v {
|
||||
normalized[i] = normalizeMultipartValue(vItem)
|
||||
}
|
||||
rv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(normalized))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// anything else
|
||||
return echo.BindBody(c, i)
|
||||
}
|
||||
|
||||
// In order to support more seamlessly both json and multipart/form-data requests,
|
||||
// the following normalization rules are applied for plain multipart string values:
|
||||
// - "true" is converted to the json `true`
|
||||
// - "false" is converted to the json `false`
|
||||
// - numeric (non-scientific) strings are converted to json number
|
||||
// - any other string (empty string too) is left as it is
|
||||
func normalizeMultipartValue(raw string) any {
|
||||
switch raw {
|
||||
case "":
|
||||
return raw
|
||||
case "true":
|
||||
return true
|
||||
case "false":
|
||||
return false
|
||||
default:
|
||||
if raw[0] == '-' || (raw[0] >= '0' && raw[0] <= '9') {
|
||||
if v, err := cast.ToFloat64E(raw); err == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
return raw
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package rest_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -16,31 +17,40 @@ func TestBindBody(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
body io.Reader
|
||||
contentType string
|
||||
result map[string]string
|
||||
expectBody string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
strings.NewReader(""),
|
||||
echo.MIMEApplicationJSON,
|
||||
map[string]string{},
|
||||
`{}`,
|
||||
false,
|
||||
},
|
||||
{
|
||||
strings.NewReader(`{"test":"invalid`),
|
||||
echo.MIMEApplicationJSON,
|
||||
map[string]string{},
|
||||
`{}`,
|
||||
true,
|
||||
},
|
||||
{
|
||||
strings.NewReader(`{"test":"test123"}`),
|
||||
strings.NewReader(`{"test":123}`),
|
||||
echo.MIMEApplicationJSON,
|
||||
map[string]string{"test": "test123"},
|
||||
`{"test":123}`,
|
||||
false,
|
||||
},
|
||||
{
|
||||
strings.NewReader(url.Values{"test": []string{"test123"}}.Encode()),
|
||||
strings.NewReader(
|
||||
url.Values{
|
||||
"string": []string{"str"},
|
||||
"stings": []string{"str1", "str2", ""},
|
||||
"number": []string{"-123"},
|
||||
"numbers": []string{"123", "456.789"},
|
||||
"bool": []string{"true"},
|
||||
"bools": []string{"true", "false"},
|
||||
}.Encode(),
|
||||
),
|
||||
echo.MIMEApplicationForm,
|
||||
map[string]string{"test": "test123"},
|
||||
`{"bool":true,"bools":[true,false],"number":-123,"numbers":[123,456.789],"stings":["str1","str2",""],"string":"str"}`,
|
||||
false,
|
||||
},
|
||||
}
|
||||
@@ -52,25 +62,22 @@ func TestBindBody(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
|
||||
result := map[string]string{}
|
||||
err := rest.BindBody(c, &result)
|
||||
data := map[string]any{}
|
||||
err := rest.BindBody(c, &data)
|
||||
|
||||
if err == nil && scenario.expectError {
|
||||
t.Errorf("(%d) Expected error, got nil", i)
|
||||
hasErr := err != nil
|
||||
if hasErr != scenario.expectError {
|
||||
t.Errorf("[%d] Expected hasErr %v, got %v", i, scenario.expectError, hasErr)
|
||||
}
|
||||
|
||||
if err != nil && !scenario.expectError {
|
||||
t.Errorf("(%d) Expected nil, got error %v", i, err)
|
||||
rawBody, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
t.Errorf("[%d] Failed to marshal binded body: %v", i, err)
|
||||
|
||||
}
|
||||
|
||||
if len(result) != len(scenario.result) {
|
||||
t.Errorf("(%d) Expected %v, got %v", i, scenario.result, result)
|
||||
}
|
||||
|
||||
for k, v := range result {
|
||||
if sv, ok := scenario.result[k]; !ok || v != sv {
|
||||
t.Errorf("(%d) Expected value %v for key %s, got %v", i, sv, k, v)
|
||||
}
|
||||
if scenario.expectBody != string(rawBody) {
|
||||
t.Errorf("[%d] Expected body \n%s, \ngot \n%s", i, scenario.expectBody, rawBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ type Result struct {
|
||||
type Provider struct {
|
||||
fieldResolver FieldResolver
|
||||
query *dbx.SelectQuery
|
||||
countCol string
|
||||
page int
|
||||
perPage int
|
||||
sort []SortField
|
||||
@@ -56,6 +57,7 @@ type Provider struct {
|
||||
func NewProvider(fieldResolver FieldResolver) *Provider {
|
||||
return &Provider{
|
||||
fieldResolver: fieldResolver,
|
||||
countCol: "_rowid_",
|
||||
page: 1,
|
||||
perPage: DefaultPerPage,
|
||||
sort: []SortField{},
|
||||
@@ -69,6 +71,13 @@ func (s *Provider) Query(query *dbx.SelectQuery) *Provider {
|
||||
return s
|
||||
}
|
||||
|
||||
// CountCol allows changing the default column (_rowid_) that is used
|
||||
// to generated the COUNT SQL query statement.
|
||||
func (s *Provider) CountCol(name string) *Provider {
|
||||
s.countCol = name
|
||||
return s
|
||||
}
|
||||
|
||||
// Page sets the `page` field of the current search provider.
|
||||
//
|
||||
// Normalization on the `page` value is done during `Exec()`.
|
||||
@@ -198,7 +207,7 @@ func (s *Provider) Exec(items any) (*Result, error) {
|
||||
baseTable = queryInfo.From[0]
|
||||
}
|
||||
clone := modelsQuery
|
||||
countQuery := clone.Select("COUNT(DISTINCT [[" + baseTable + ".id]])").OrderBy()
|
||||
countQuery := clone.Distinct(false).Select("COUNT(DISTINCT [[" + baseTable + "." + s.countCol + "]])").OrderBy()
|
||||
if err := countQuery.Row(&totalCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -42,6 +42,20 @@ func TestProviderQuery(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderCountCol(t *testing.T) {
|
||||
p := NewProvider(&testFieldResolver{})
|
||||
|
||||
if p.countCol != "_rowid_" {
|
||||
t.Fatalf("Expected the default countCol to be %s, got %s", "_rowid_", p.countCol)
|
||||
}
|
||||
|
||||
p.CountCol("test")
|
||||
|
||||
if p.countCol != "test" {
|
||||
t.Fatalf("Expected colCount to change to %s, got %s", "test", p.countCol)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderPage(t *testing.T) {
|
||||
r := &testFieldResolver{}
|
||||
p := NewProvider(r).Page(10)
|
||||
@@ -228,7 +242,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
|
||||
false,
|
||||
`{"page":1,"perPage":10,"totalItems":2,"totalPages":1,"items":[{"test1":1,"test2":"test2.1","test3":""},{"test1":2,"test2":"test2.2","test3":""}]}`,
|
||||
[]string{
|
||||
"SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE NOT (`test1` IS NULL)",
|
||||
"SELECT COUNT(DISTINCT [[test._rowid_]]) FROM `test` WHERE NOT (`test1` IS NULL)",
|
||||
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 10",
|
||||
},
|
||||
},
|
||||
@@ -241,7 +255,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
|
||||
false,
|
||||
`{"page":1,"perPage":30,"totalItems":2,"totalPages":1,"items":[{"test1":1,"test2":"test2.1","test3":""},{"test1":2,"test2":"test2.2","test3":""}]}`,
|
||||
[]string{
|
||||
"SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE NOT (`test1` IS NULL)",
|
||||
"SELECT COUNT(DISTINCT [[test._rowid_]]) FROM `test` WHERE NOT (`test1` IS NULL)",
|
||||
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 30",
|
||||
},
|
||||
},
|
||||
@@ -274,7 +288,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
|
||||
false,
|
||||
`{"page":1,"perPage":` + fmt.Sprint(MaxPerPage) + `,"totalItems":1,"totalPages":1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
|
||||
[]string{
|
||||
"SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (((test2 != '' AND test2 IS NOT NULL)))) AND (test1 >= 2)",
|
||||
"SELECT COUNT(DISTINCT [[test._rowid_]]) FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (((test2 != '' AND test2 IS NOT NULL)))) AND (test1 >= 2)",
|
||||
"SELECT * FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (((test2 != '' AND test2 IS NOT NULL)))) AND (test1 >= 2) ORDER BY `test1` ASC, `test2` DESC LIMIT 500",
|
||||
},
|
||||
},
|
||||
@@ -287,7 +301,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
|
||||
false,
|
||||
`{"page":1,"perPage":10,"totalItems":0,"totalPages":0,"items":[]}`,
|
||||
[]string{
|
||||
"SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE (NOT (`test1` IS NULL)) AND (((test3 != '' AND test3 IS NOT NULL)))",
|
||||
"SELECT COUNT(DISTINCT [[test._rowid_]]) FROM `test` WHERE (NOT (`test1` IS NULL)) AND (((test3 != '' AND test3 IS NOT NULL)))",
|
||||
"SELECT * FROM `test` WHERE (NOT (`test1` IS NULL)) AND (((test3 != '' AND test3 IS NOT NULL))) ORDER BY `test1` ASC, `test3` ASC LIMIT 10",
|
||||
},
|
||||
},
|
||||
@@ -300,7 +314,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
|
||||
false,
|
||||
`{"page":2,"perPage":1,"totalItems":2,"totalPages":2,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
|
||||
[]string{
|
||||
"SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE NOT (`test1` IS NULL)",
|
||||
"SELECT COUNT(DISTINCT [[test._rowid_]]) FROM `test` WHERE NOT (`test1` IS NULL)",
|
||||
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 1 OFFSET 1",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -78,7 +78,7 @@ func (d *DateTime) Scan(value any) error {
|
||||
d.t = v.Time()
|
||||
case time.Time:
|
||||
d.t = v
|
||||
case int:
|
||||
case int, int64, int32, uint, uint64, uint32:
|
||||
d.t = cast.ToTime(v)
|
||||
case string:
|
||||
if v == "" {
|
||||
|
||||
@@ -32,6 +32,11 @@ func TestParseDateTime(t *testing.T) {
|
||||
{nowDateTime, nowStr},
|
||||
{nowTime, nowStr},
|
||||
{1641024040, "2022-01-01 08:00:40.000Z"},
|
||||
{int32(1641024040), "2022-01-01 08:00:40.000Z"},
|
||||
{int64(1641024040), "2022-01-01 08:00:40.000Z"},
|
||||
{uint(1641024040), "2022-01-01 08:00:40.000Z"},
|
||||
{uint64(1641024040), "2022-01-01 08:00:40.000Z"},
|
||||
{uint32(1641024040), "2022-01-01 08:00:40.000Z"},
|
||||
{"2022-01-01 11:23:45.678", "2022-01-01 11:23:45.678Z"},
|
||||
}
|
||||
|
||||
|
||||
@@ -9,4 +9,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.15.2"
|
||||
PB_VERSION = "v0.16.9"
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
import{S as ke,i as be,s as ge,e as r,w as g,b as w,c as _e,f as k,g as h,h as n,m as me,x as H,N as re,O as we,k as ve,P as Ce,n as Pe,t as L,a as Y,o as _,d as pe,T as Me,C as Se,p as $e,r as Q,u as je,M as Ae}from"./index-79bb649c.js";import{S as Be}from"./SdkTabs-22bd33d6.js";function ue(a,l,o){const s=a.slice();return s[5]=l[o],s}function de(a,l,o){const s=a.slice();return s[5]=l[o],s}function fe(a,l){let o,s=l[5].code+"",m,f,i,u;function d(){return l[4](l[5])}return{key:a,first:null,c(){o=r("button"),m=g(s),f=w(),k(o,"class","tab-item"),Q(o,"active",l[1]===l[5].code),this.first=o},m(v,C){h(v,o,C),n(o,m),n(o,f),i||(u=je(o,"click",d),i=!0)},p(v,C){l=v,C&4&&s!==(s=l[5].code+"")&&H(m,s),C&6&&Q(o,"active",l[1]===l[5].code)},d(v){v&&_(o),i=!1,u()}}}function he(a,l){let o,s,m,f;return s=new Ae({props:{content:l[5].body}}),{key:a,first:null,c(){o=r("div"),_e(s.$$.fragment),m=w(),k(o,"class","tab-item"),Q(o,"active",l[1]===l[5].code),this.first=o},m(i,u){h(i,o,u),me(s,o,null),n(o,m),f=!0},p(i,u){l=i;const d={};u&4&&(d.content=l[5].body),s.$set(d),(!f||u&6)&&Q(o,"active",l[1]===l[5].code)},i(i){f||(L(s.$$.fragment,i),f=!0)},o(i){Y(s.$$.fragment,i),f=!1},d(i){i&&_(o),pe(s)}}}function Oe(a){var ae,ne;let l,o,s=a[0].name+"",m,f,i,u,d,v,C,F=a[0].name+"",U,X,q,P,D,j,W,M,K,R,y,A,Z,V,z=a[0].name+"",E,x,I,B,J,S,O,b=[],ee=new Map,te,T,p=[],le=new Map,$;P=new Be({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${a[3]}');
|
||||
|
||||
...
|
||||
|
||||
const result = await pb.collection('${(ae=a[0])==null?void 0:ae.name}').listAuthMethods();
|
||||
`,dart:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${a[3]}');
|
||||
|
||||
...
|
||||
|
||||
final result = await pb.collection('${(ne=a[0])==null?void 0:ne.name}').listAuthMethods();
|
||||
`}});let G=a[2];const oe=e=>e[5].code;for(let e=0;e<G.length;e+=1){let t=de(a,G,e),c=oe(t);ee.set(c,b[e]=fe(c,t))}let N=a[2];const se=e=>e[5].code;for(let e=0;e<N.length;e+=1){let t=ue(a,N,e),c=se(t);le.set(c,p[e]=he(c,t))}return{c(){l=r("h3"),o=g("List auth methods ("),m=g(s),f=g(")"),i=w(),u=r("div"),d=r("p"),v=g("Returns a public list with all allowed "),C=r("strong"),U=g(F),X=g(" authentication methods."),q=w(),_e(P.$$.fragment),D=w(),j=r("h6"),j.textContent="API details",W=w(),M=r("div"),K=r("strong"),K.textContent="GET",R=w(),y=r("div"),A=r("p"),Z=g("/api/collections/"),V=r("strong"),E=g(z),x=g("/auth-methods"),I=w(),B=r("div"),B.textContent="Responses",J=w(),S=r("div"),O=r("div");for(let e=0;e<b.length;e+=1)b[e].c();te=w(),T=r("div");for(let e=0;e<p.length;e+=1)p[e].c();k(l,"class","m-b-sm"),k(u,"class","content txt-lg m-b-sm"),k(j,"class","m-b-xs"),k(K,"class","label label-primary"),k(y,"class","content"),k(M,"class","alert alert-info"),k(B,"class","section-title"),k(O,"class","tabs-header compact left"),k(T,"class","tabs-content"),k(S,"class","tabs")},m(e,t){h(e,l,t),n(l,o),n(l,m),n(l,f),h(e,i,t),h(e,u,t),n(u,d),n(d,v),n(d,C),n(C,U),n(d,X),h(e,q,t),me(P,e,t),h(e,D,t),h(e,j,t),h(e,W,t),h(e,M,t),n(M,K),n(M,R),n(M,y),n(y,A),n(A,Z),n(A,V),n(V,E),n(A,x),h(e,I,t),h(e,B,t),h(e,J,t),h(e,S,t),n(S,O);for(let c=0;c<b.length;c+=1)b[c]&&b[c].m(O,null);n(S,te),n(S,T);for(let c=0;c<p.length;c+=1)p[c]&&p[c].m(T,null);$=!0},p(e,[t]){var ie,ce;(!$||t&1)&&s!==(s=e[0].name+"")&&H(m,s),(!$||t&1)&&F!==(F=e[0].name+"")&&H(U,F);const c={};t&9&&(c.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
const result = await pb.collection('${(ie=e[0])==null?void 0:ie.name}').listAuthMethods();
|
||||
`),t&9&&(c.dart=`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
final result = await pb.collection('${(ce=e[0])==null?void 0:ce.name}').listAuthMethods();
|
||||
`),P.$set(c),(!$||t&1)&&z!==(z=e[0].name+"")&&H(E,z),t&6&&(G=e[2],b=re(b,t,oe,1,e,G,ee,O,we,fe,null,de)),t&6&&(N=e[2],ve(),p=re(p,t,se,1,e,N,le,T,Ce,he,null,ue),Pe())},i(e){if(!$){L(P.$$.fragment,e);for(let t=0;t<N.length;t+=1)L(p[t]);$=!0}},o(e){Y(P.$$.fragment,e);for(let t=0;t<p.length;t+=1)Y(p[t]);$=!1},d(e){e&&_(l),e&&_(i),e&&_(u),e&&_(q),pe(P,e),e&&_(D),e&&_(j),e&&_(W),e&&_(M),e&&_(I),e&&_(B),e&&_(J),e&&_(S);for(let t=0;t<b.length;t+=1)b[t].d();for(let t=0;t<p.length;t+=1)p[t].d()}}}function Te(a,l,o){let s,{collection:m=new Me}=l,f=200,i=[];const u=d=>o(1,f=d.code);return a.$$set=d=>{"collection"in d&&o(0,m=d.collection)},o(3,s=Se.getApiExampleUrl($e.baseUrl)),o(2,i=[{code:200,body:`
|
||||
{
|
||||
"usernamePassword": true,
|
||||
"emailPassword": true,
|
||||
"authProviders": [
|
||||
{
|
||||
"name": "github",
|
||||
"state": "3Yd8jNkK_6PJG6hPWwBjLqKwse6Ejd",
|
||||
"codeVerifier": "KxFDWz1B3fxscCDJ_9gHQhLuh__ie7",
|
||||
"codeChallenge": "NM1oVexB6Q6QH8uPtOUfK7tq4pmu4Jz6lNDIwoxHZNE=",
|
||||
"codeChallengeMethod": "S256",
|
||||
"authUrl": "https://github.com/login/oauth/authorize?client_id=demo&code_challenge=NM1oVexB6Q6QH8uPtOUfK7tq4pmu4Jz6lNDIwoxHZNE%3D&code_challenge_method=S256&response_type=code&scope=user&state=3Yd8jNkK_6PJG6hPWwBjLqKwse6Ejd&redirect_uri="
|
||||
},
|
||||
{
|
||||
"name": "gitlab",
|
||||
"state": "NeQSbtO5cShr_mk5__3CUukiMnymeb",
|
||||
"codeVerifier": "ahTFHOgua8mkvPAlIBGwCUJbWKR_xi",
|
||||
"codeChallenge": "O-GATkTj4eXDCnfonsqGLCd6njvTixlpCMvy5kjgOOg=",
|
||||
"codeChallengeMethod": "S256",
|
||||
"authUrl": "https://gitlab.com/oauth/authorize?client_id=demo&code_challenge=O-GATkTj4eXDCnfonsqGLCd6njvTixlpCMvy5kjgOOg%3D&code_challenge_method=S256&response_type=code&scope=read_user&state=NeQSbtO5cShr_mk5__3CUukiMnymeb&redirect_uri="
|
||||
},
|
||||
{
|
||||
"name": "google",
|
||||
"state": "zB3ZPifV1TW2GMuvuFkamSXfSNkHPQ",
|
||||
"codeVerifier": "t3CmO5VObGzdXqieakvR_fpjiW0zdO",
|
||||
"codeChallenge": "KChwoQPKYlz2anAdqtgsSTdIo8hdwtc1fh2wHMwW2Yk=",
|
||||
"codeChallengeMethod": "S256",
|
||||
"authUrl": "https://accounts.google.com/o/oauth2/auth?client_id=demo&code_challenge=KChwoQPKYlz2anAdqtgsSTdIo8hdwtc1fh2wHMwW2Yk%3D&code_challenge_method=S256&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&state=zB3ZPifV1TW2GMuvuFkamSXfSNkHPQ&redirect_uri="
|
||||
}
|
||||
]
|
||||
}
|
||||
`}]),[m,f,i,s,u]}class Ke extends ke{constructor(l){super(),be(this,l,Te,Oe,ge,{collection:0})}}export{Ke as default};
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import{S as $e,i as Se,s as ye,e as c,w,b as k,c as oe,f as h,g as d,h as a,m as se,x as G,N as we,P as Te,k as je,Q as Ae,n as Be,t as U,a as W,o as u,d as ae,T as Fe,C as Oe,p as Qe,r as V,u as Ne,M as He}from"./index-6e04d404.js";import{S as Ke}from"./SdkTabs-1a82fff4.js";import{F as qe}from"./FieldsQueryParam-db646c6c.js";function ve(n,l,o){const s=n.slice();return s[5]=l[o],s}function Ce(n,l,o){const s=n.slice();return s[5]=l[o],s}function Pe(n,l){let o,s=l[5].code+"",_,p,i,f;function m(){return l[4](l[5])}return{key:n,first:null,c(){o=c("button"),_=w(s),p=k(),h(o,"class","tab-item"),V(o,"active",l[1]===l[5].code),this.first=o},m(v,C){d(v,o,C),a(o,_),a(o,p),i||(f=Ne(o,"click",m),i=!0)},p(v,C){l=v,C&4&&s!==(s=l[5].code+"")&&G(_,s),C&6&&V(o,"active",l[1]===l[5].code)},d(v){v&&u(o),i=!1,f()}}}function Me(n,l){let o,s,_,p;return s=new He({props:{content:l[5].body}}),{key:n,first:null,c(){o=c("div"),oe(s.$$.fragment),_=k(),h(o,"class","tab-item"),V(o,"active",l[1]===l[5].code),this.first=o},m(i,f){d(i,o,f),se(s,o,null),a(o,_),p=!0},p(i,f){l=i;const m={};f&4&&(m.content=l[5].body),s.$set(m),(!p||f&6)&&V(o,"active",l[1]===l[5].code)},i(i){p||(U(s.$$.fragment,i),p=!0)},o(i){W(s.$$.fragment,i),p=!1},d(i){i&&u(o),ae(s)}}}function ze(n){var _e,be;let l,o,s=n[0].name+"",_,p,i,f,m,v,C,H=n[0].name+"",L,ne,E,P,I,j,J,M,K,ie,q,A,ce,Y,z=n[0].name+"",X,re,R,B,Z,$,x,de,ee,T,te,F,le,S,O,g=[],ue=new Map,fe,Q,b=[],me=new Map,y;P=new Ke({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${n[3]}');
|
||||
|
||||
...
|
||||
|
||||
const result = await pb.collection('${(_e=n[0])==null?void 0:_e.name}').listAuthMethods();
|
||||
`,dart:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${n[3]}');
|
||||
|
||||
...
|
||||
|
||||
final result = await pb.collection('${(be=n[0])==null?void 0:be.name}').listAuthMethods();
|
||||
`}}),T=new qe({});let D=n[2];const pe=e=>e[5].code;for(let e=0;e<D.length;e+=1){let t=Ce(n,D,e),r=pe(t);ue.set(r,g[e]=Pe(r,t))}let N=n[2];const he=e=>e[5].code;for(let e=0;e<N.length;e+=1){let t=ve(n,N,e),r=he(t);me.set(r,b[e]=Me(r,t))}return{c(){l=c("h3"),o=w("List auth methods ("),_=w(s),p=w(")"),i=k(),f=c("div"),m=c("p"),v=w("Returns a public list with all allowed "),C=c("strong"),L=w(H),ne=w(" authentication methods."),E=k(),oe(P.$$.fragment),I=k(),j=c("h6"),j.textContent="API details",J=k(),M=c("div"),K=c("strong"),K.textContent="GET",ie=k(),q=c("div"),A=c("p"),ce=w("/api/collections/"),Y=c("strong"),X=w(z),re=w("/auth-methods"),R=k(),B=c("div"),B.textContent="Query parameters",Z=k(),$=c("table"),x=c("thead"),x.innerHTML=`<tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th></tr>`,de=k(),ee=c("tbody"),oe(T.$$.fragment),te=k(),F=c("div"),F.textContent="Responses",le=k(),S=c("div"),O=c("div");for(let e=0;e<g.length;e+=1)g[e].c();fe=k(),Q=c("div");for(let e=0;e<b.length;e+=1)b[e].c();h(l,"class","m-b-sm"),h(f,"class","content txt-lg m-b-sm"),h(j,"class","m-b-xs"),h(K,"class","label label-primary"),h(q,"class","content"),h(M,"class","alert alert-info"),h(B,"class","section-title"),h($,"class","table-compact table-border m-b-base"),h(F,"class","section-title"),h(O,"class","tabs-header compact left"),h(Q,"class","tabs-content"),h(S,"class","tabs")},m(e,t){d(e,l,t),a(l,o),a(l,_),a(l,p),d(e,i,t),d(e,f,t),a(f,m),a(m,v),a(m,C),a(C,L),a(m,ne),d(e,E,t),se(P,e,t),d(e,I,t),d(e,j,t),d(e,J,t),d(e,M,t),a(M,K),a(M,ie),a(M,q),a(q,A),a(A,ce),a(A,Y),a(Y,X),a(A,re),d(e,R,t),d(e,B,t),d(e,Z,t),d(e,$,t),a($,x),a($,de),a($,ee),se(T,ee,null),d(e,te,t),d(e,F,t),d(e,le,t),d(e,S,t),a(S,O);for(let r=0;r<g.length;r+=1)g[r]&&g[r].m(O,null);a(S,fe),a(S,Q);for(let r=0;r<b.length;r+=1)b[r]&&b[r].m(Q,null);y=!0},p(e,[t]){var ke,ge;(!y||t&1)&&s!==(s=e[0].name+"")&&G(_,s),(!y||t&1)&&H!==(H=e[0].name+"")&&G(L,H);const r={};t&9&&(r.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
const result = await pb.collection('${(ke=e[0])==null?void 0:ke.name}').listAuthMethods();
|
||||
`),t&9&&(r.dart=`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
final result = await pb.collection('${(ge=e[0])==null?void 0:ge.name}').listAuthMethods();
|
||||
`),P.$set(r),(!y||t&1)&&z!==(z=e[0].name+"")&&G(X,z),t&6&&(D=e[2],g=we(g,t,pe,1,e,D,ue,O,Te,Pe,null,Ce)),t&6&&(N=e[2],je(),b=we(b,t,he,1,e,N,me,Q,Ae,Me,null,ve),Be())},i(e){if(!y){U(P.$$.fragment,e),U(T.$$.fragment,e);for(let t=0;t<N.length;t+=1)U(b[t]);y=!0}},o(e){W(P.$$.fragment,e),W(T.$$.fragment,e);for(let t=0;t<b.length;t+=1)W(b[t]);y=!1},d(e){e&&u(l),e&&u(i),e&&u(f),e&&u(E),ae(P,e),e&&u(I),e&&u(j),e&&u(J),e&&u(M),e&&u(R),e&&u(B),e&&u(Z),e&&u($),ae(T),e&&u(te),e&&u(F),e&&u(le),e&&u(S);for(let t=0;t<g.length;t+=1)g[t].d();for(let t=0;t<b.length;t+=1)b[t].d()}}}function De(n,l,o){let s,{collection:_=new Fe}=l,p=200,i=[];const f=m=>o(1,p=m.code);return n.$$set=m=>{"collection"in m&&o(0,_=m.collection)},o(3,s=Oe.getApiExampleUrl(Qe.baseUrl)),o(2,i=[{code:200,body:`
|
||||
{
|
||||
"usernamePassword": true,
|
||||
"emailPassword": true,
|
||||
"authProviders": [
|
||||
{
|
||||
"name": "github",
|
||||
"state": "3Yd8jNkK_6PJG6hPWwBjLqKwse6Ejd",
|
||||
"codeVerifier": "KxFDWz1B3fxscCDJ_9gHQhLuh__ie7",
|
||||
"codeChallenge": "NM1oVexB6Q6QH8uPtOUfK7tq4pmu4Jz6lNDIwoxHZNE=",
|
||||
"codeChallengeMethod": "S256",
|
||||
"authUrl": "https://github.com/login/oauth/authorize?client_id=demo&code_challenge=NM1oVexB6Q6QH8uPtOUfK7tq4pmu4Jz6lNDIwoxHZNE%3D&code_challenge_method=S256&response_type=code&scope=user&state=3Yd8jNkK_6PJG6hPWwBjLqKwse6Ejd&redirect_uri="
|
||||
},
|
||||
{
|
||||
"name": "gitlab",
|
||||
"state": "NeQSbtO5cShr_mk5__3CUukiMnymeb",
|
||||
"codeVerifier": "ahTFHOgua8mkvPAlIBGwCUJbWKR_xi",
|
||||
"codeChallenge": "O-GATkTj4eXDCnfonsqGLCd6njvTixlpCMvy5kjgOOg=",
|
||||
"codeChallengeMethod": "S256",
|
||||
"authUrl": "https://gitlab.com/oauth/authorize?client_id=demo&code_challenge=O-GATkTj4eXDCnfonsqGLCd6njvTixlpCMvy5kjgOOg%3D&code_challenge_method=S256&response_type=code&scope=read_user&state=NeQSbtO5cShr_mk5__3CUukiMnymeb&redirect_uri="
|
||||
},
|
||||
{
|
||||
"name": "google",
|
||||
"state": "zB3ZPifV1TW2GMuvuFkamSXfSNkHPQ",
|
||||
"codeVerifier": "t3CmO5VObGzdXqieakvR_fpjiW0zdO",
|
||||
"codeChallenge": "KChwoQPKYlz2anAdqtgsSTdIo8hdwtc1fh2wHMwW2Yk=",
|
||||
"codeChallengeMethod": "S256",
|
||||
"authUrl": "https://accounts.google.com/o/oauth2/auth?client_id=demo&code_challenge=KChwoQPKYlz2anAdqtgsSTdIo8hdwtc1fh2wHMwW2Yk%3D&code_challenge_method=S256&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&state=zB3ZPifV1TW2GMuvuFkamSXfSNkHPQ&redirect_uri="
|
||||
}
|
||||
]
|
||||
}
|
||||
`}]),[_,p,i,s,f]}class Ve extends $e{constructor(l){super(),Se(this,l,De,ze,ye,{collection:0})}}export{Ve as default};
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
import{S as ze,i as Ue,s as je,M as Ve,e as a,w as k,b as p,c as ae,f as b,g as d,h as o,m as ne,x as re,N as qe,O as xe,k as Je,P as Ke,n as Ie,t as U,a as j,o as u,d as ie,T as Qe,C as He,p as We,r as x,u as Ge}from"./index-79bb649c.js";import{S as Xe}from"./SdkTabs-22bd33d6.js";function Ee(r,l,s){const n=r.slice();return n[5]=l[s],n}function Fe(r,l,s){const n=r.slice();return n[5]=l[s],n}function Le(r,l){let s,n=l[5].code+"",m,_,i,f;function v(){return l[4](l[5])}return{key:r,first:null,c(){s=a("button"),m=k(n),_=p(),b(s,"class","tab-item"),x(s,"active",l[1]===l[5].code),this.first=s},m(g,w){d(g,s,w),o(s,m),o(s,_),i||(f=Ge(s,"click",v),i=!0)},p(g,w){l=g,w&4&&n!==(n=l[5].code+"")&&re(m,n),w&6&&x(s,"active",l[1]===l[5].code)},d(g){g&&u(s),i=!1,f()}}}function Ne(r,l){let s,n,m,_;return n=new Ve({props:{content:l[5].body}}),{key:r,first:null,c(){s=a("div"),ae(n.$$.fragment),m=p(),b(s,"class","tab-item"),x(s,"active",l[1]===l[5].code),this.first=s},m(i,f){d(i,s,f),ne(n,s,null),o(s,m),_=!0},p(i,f){l=i;const v={};f&4&&(v.content=l[5].body),n.$set(v),(!_||f&6)&&x(s,"active",l[1]===l[5].code)},i(i){_||(U(n.$$.fragment,i),_=!0)},o(i){j(n.$$.fragment,i),_=!1},d(i){i&&u(s),ie(n)}}}function Ye(r){var Ae,Be;let l,s,n=r[0].name+"",m,_,i,f,v,g,w,A,J,$,F,ce,L,B,de,K,N=r[0].name+"",I,ue,pe,V,Q,D,W,T,G,fe,X,C,Y,he,Z,be,h,me,R,_e,ke,ve,ee,ge,te,ye,Se,$e,oe,we,le,O,se,P,q,S=[],Te=new Map,Ce,H,y=[],Pe=new Map,M;g=new Xe({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${r[3]}');
|
||||
|
||||
...
|
||||
|
||||
const authData = await pb.collection('${(Ae=r[0])==null?void 0:Ae.name}').authRefresh();
|
||||
|
||||
// after the above you can also access the refreshed auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
`,dart:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${r[3]}');
|
||||
|
||||
...
|
||||
|
||||
final authData = await pb.collection('${(Be=r[0])==null?void 0:Be.name}').authRefresh();
|
||||
|
||||
// after the above you can also access the refreshed auth data from the authStore
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.model.id);
|
||||
`}}),R=new Ve({props:{content:"?expand=relField1,relField2.subRelField"}});let z=r[2];const Re=e=>e[5].code;for(let e=0;e<z.length;e+=1){let t=Fe(r,z,e),c=Re(t);Te.set(c,S[e]=Le(c,t))}let E=r[2];const Me=e=>e[5].code;for(let e=0;e<E.length;e+=1){let t=Ee(r,E,e),c=Me(t);Pe.set(c,y[e]=Ne(c,t))}return{c(){l=a("h3"),s=k("Auth refresh ("),m=k(n),_=k(")"),i=p(),f=a("div"),f.innerHTML=`<p>Returns a new auth response (token and record data) for an
|
||||
<strong>already authenticated record</strong>.</p>
|
||||
<p><em>This method is usually called by users on page/screen reload to ensure that the previously stored
|
||||
data in <code>pb.authStore</code> is still valid and up-to-date.</em></p>`,v=p(),ae(g.$$.fragment),w=p(),A=a("h6"),A.textContent="API details",J=p(),$=a("div"),F=a("strong"),F.textContent="POST",ce=p(),L=a("div"),B=a("p"),de=k("/api/collections/"),K=a("strong"),I=k(N),ue=k("/auth-refresh"),pe=p(),V=a("p"),V.innerHTML="Requires record <code>Authorization:TOKEN</code> header",Q=p(),D=a("div"),D.textContent="Query parameters",W=p(),T=a("table"),G=a("thead"),G.innerHTML=`<tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th></tr>`,fe=p(),X=a("tbody"),C=a("tr"),Y=a("td"),Y.textContent="expand",he=p(),Z=a("td"),Z.innerHTML='<span class="label">String</span>',be=p(),h=a("td"),me=k(`Auto expand record relations. Ex.:
|
||||
`),ae(R.$$.fragment),_e=k(`
|
||||
Supports up to 6-levels depth nested relations expansion. `),ke=a("br"),ve=k(`
|
||||
The expanded relations will be appended to the record under the
|
||||
`),ee=a("code"),ee.textContent="expand",ge=k(" property (eg. "),te=a("code"),te.textContent='"expand": {"relField1": {...}, ...}',ye=k(`).
|
||||
`),Se=a("br"),$e=k(`
|
||||
Only the relations to which the request user has permissions to `),oe=a("strong"),oe.textContent="view",we=k(" will be expanded."),le=p(),O=a("div"),O.textContent="Responses",se=p(),P=a("div"),q=a("div");for(let e=0;e<S.length;e+=1)S[e].c();Ce=p(),H=a("div");for(let e=0;e<y.length;e+=1)y[e].c();b(l,"class","m-b-sm"),b(f,"class","content txt-lg m-b-sm"),b(A,"class","m-b-xs"),b(F,"class","label label-primary"),b(L,"class","content"),b(V,"class","txt-hint txt-sm txt-right"),b($,"class","alert alert-success"),b(D,"class","section-title"),b(T,"class","table-compact table-border m-b-base"),b(O,"class","section-title"),b(q,"class","tabs-header compact left"),b(H,"class","tabs-content"),b(P,"class","tabs")},m(e,t){d(e,l,t),o(l,s),o(l,m),o(l,_),d(e,i,t),d(e,f,t),d(e,v,t),ne(g,e,t),d(e,w,t),d(e,A,t),d(e,J,t),d(e,$,t),o($,F),o($,ce),o($,L),o(L,B),o(B,de),o(B,K),o(K,I),o(B,ue),o($,pe),o($,V),d(e,Q,t),d(e,D,t),d(e,W,t),d(e,T,t),o(T,G),o(T,fe),o(T,X),o(X,C),o(C,Y),o(C,he),o(C,Z),o(C,be),o(C,h),o(h,me),ne(R,h,null),o(h,_e),o(h,ke),o(h,ve),o(h,ee),o(h,ge),o(h,te),o(h,ye),o(h,Se),o(h,$e),o(h,oe),o(h,we),d(e,le,t),d(e,O,t),d(e,se,t),d(e,P,t),o(P,q);for(let c=0;c<S.length;c+=1)S[c]&&S[c].m(q,null);o(P,Ce),o(P,H);for(let c=0;c<y.length;c+=1)y[c]&&y[c].m(H,null);M=!0},p(e,[t]){var De,Oe;(!M||t&1)&&n!==(n=e[0].name+"")&&re(m,n);const c={};t&9&&(c.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
const authData = await pb.collection('${(De=e[0])==null?void 0:De.name}').authRefresh();
|
||||
|
||||
// after the above you can also access the refreshed auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
`),t&9&&(c.dart=`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
final authData = await pb.collection('${(Oe=e[0])==null?void 0:Oe.name}').authRefresh();
|
||||
|
||||
// after the above you can also access the refreshed auth data from the authStore
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.model.id);
|
||||
`),g.$set(c),(!M||t&1)&&N!==(N=e[0].name+"")&&re(I,N),t&6&&(z=e[2],S=qe(S,t,Re,1,e,z,Te,q,xe,Le,null,Fe)),t&6&&(E=e[2],Je(),y=qe(y,t,Me,1,e,E,Pe,H,Ke,Ne,null,Ee),Ie())},i(e){if(!M){U(g.$$.fragment,e),U(R.$$.fragment,e);for(let t=0;t<E.length;t+=1)U(y[t]);M=!0}},o(e){j(g.$$.fragment,e),j(R.$$.fragment,e);for(let t=0;t<y.length;t+=1)j(y[t]);M=!1},d(e){e&&u(l),e&&u(i),e&&u(f),e&&u(v),ie(g,e),e&&u(w),e&&u(A),e&&u(J),e&&u($),e&&u(Q),e&&u(D),e&&u(W),e&&u(T),ie(R),e&&u(le),e&&u(O),e&&u(se),e&&u(P);for(let t=0;t<S.length;t+=1)S[t].d();for(let t=0;t<y.length;t+=1)y[t].d()}}}function Ze(r,l,s){let n,{collection:m=new Qe}=l,_=200,i=[];const f=v=>s(1,_=v.code);return r.$$set=v=>{"collection"in v&&s(0,m=v.collection)},r.$$.update=()=>{r.$$.dirty&1&&s(2,i=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:He.dummyCollectionRecord(m)},null,2)},{code:401,body:`
|
||||
{
|
||||
"code": 401,
|
||||
"message": "The request requires valid record authorization token to be set.",
|
||||
"data": {}
|
||||
}
|
||||
`},{code:403,body:`
|
||||
{
|
||||
"code": 403,
|
||||
"message": "The authorized record model is not allowed to perform this action.",
|
||||
"data": {}
|
||||
}
|
||||
`},{code:404,body:`
|
||||
{
|
||||
"code": 404,
|
||||
"message": "Missing auth record context.",
|
||||
"data": {}
|
||||
}
|
||||
`}])},s(3,n=He.getApiExampleUrl(We.baseUrl)),[m,_,i,n,f]}class ot extends ze{constructor(l){super(),Ue(this,l,Ze,Ye,je,{collection:0})}}export{ot as default};
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import{S as Ue,i as je,s as xe,M as Qe,e as s,w as k,b as p,c as J,f as b,g as d,h as o,m as K,x as ce,N as Oe,P as Je,k as Ke,Q as Ie,n as We,t as N,a as V,o as u,d as I,T as Ge,C as Ee,p as Xe,r as W,u as Ye}from"./index-6e04d404.js";import{S as Ze}from"./SdkTabs-1a82fff4.js";import{F as et}from"./FieldsQueryParam-db646c6c.js";function Le(r,l,a){const n=r.slice();return n[5]=l[a],n}function Ne(r,l,a){const n=r.slice();return n[5]=l[a],n}function Ve(r,l){let a,n=l[5].code+"",m,_,i,f;function v(){return l[4](l[5])}return{key:r,first:null,c(){a=s("button"),m=k(n),_=p(),b(a,"class","tab-item"),W(a,"active",l[1]===l[5].code),this.first=a},m($,w){d($,a,w),o(a,m),o(a,_),i||(f=Ye(a,"click",v),i=!0)},p($,w){l=$,w&4&&n!==(n=l[5].code+"")&&ce(m,n),w&6&&W(a,"active",l[1]===l[5].code)},d($){$&&u(a),i=!1,f()}}}function ze(r,l){let a,n,m,_;return n=new Qe({props:{content:l[5].body}}),{key:r,first:null,c(){a=s("div"),J(n.$$.fragment),m=p(),b(a,"class","tab-item"),W(a,"active",l[1]===l[5].code),this.first=a},m(i,f){d(i,a,f),K(n,a,null),o(a,m),_=!0},p(i,f){l=i;const v={};f&4&&(v.content=l[5].body),n.$set(v),(!_||f&6)&&W(a,"active",l[1]===l[5].code)},i(i){_||(N(n.$$.fragment,i),_=!0)},o(i){V(n.$$.fragment,i),_=!1},d(i){i&&u(a),I(n)}}}function tt(r){var qe,De;let l,a,n=r[0].name+"",m,_,i,f,v,$,w,B,G,S,z,de,Q,q,ue,X,U=r[0].name+"",Y,pe,fe,j,Z,D,ee,T,te,he,F,C,oe,be,le,me,h,_e,R,ke,ve,$e,ae,ge,se,ye,Se,we,ne,Te,Ce,M,re,H,ie,P,O,y=[],Pe=new Map,Re,E,g=[],Me=new Map,A;$=new Ze({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${r[3]}');
|
||||
|
||||
...
|
||||
|
||||
const authData = await pb.collection('${(qe=r[0])==null?void 0:qe.name}').authRefresh();
|
||||
|
||||
// after the above you can also access the refreshed auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
`,dart:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${r[3]}');
|
||||
|
||||
...
|
||||
|
||||
final authData = await pb.collection('${(De=r[0])==null?void 0:De.name}').authRefresh();
|
||||
|
||||
// after the above you can also access the refreshed auth data from the authStore
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.model.id);
|
||||
`}}),R=new Qe({props:{content:"?expand=relField1,relField2.subRelField"}}),M=new et({});let x=r[2];const Ae=e=>e[5].code;for(let e=0;e<x.length;e+=1){let t=Ne(r,x,e),c=Ae(t);Pe.set(c,y[e]=Ve(c,t))}let L=r[2];const Be=e=>e[5].code;for(let e=0;e<L.length;e+=1){let t=Le(r,L,e),c=Be(t);Me.set(c,g[e]=ze(c,t))}return{c(){l=s("h3"),a=k("Auth refresh ("),m=k(n),_=k(")"),i=p(),f=s("div"),f.innerHTML=`<p>Returns a new auth response (token and record data) for an
|
||||
<strong>already authenticated record</strong>.</p>
|
||||
<p><em>This method is usually called by users on page/screen reload to ensure that the previously stored
|
||||
data in <code>pb.authStore</code> is still valid and up-to-date.</em></p>`,v=p(),J($.$$.fragment),w=p(),B=s("h6"),B.textContent="API details",G=p(),S=s("div"),z=s("strong"),z.textContent="POST",de=p(),Q=s("div"),q=s("p"),ue=k("/api/collections/"),X=s("strong"),Y=k(U),pe=k("/auth-refresh"),fe=p(),j=s("p"),j.innerHTML="Requires record <code>Authorization:TOKEN</code> header",Z=p(),D=s("div"),D.textContent="Query parameters",ee=p(),T=s("table"),te=s("thead"),te.innerHTML=`<tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th></tr>`,he=p(),F=s("tbody"),C=s("tr"),oe=s("td"),oe.textContent="expand",be=p(),le=s("td"),le.innerHTML='<span class="label">String</span>',me=p(),h=s("td"),_e=k(`Auto expand record relations. Ex.:
|
||||
`),J(R.$$.fragment),ke=k(`
|
||||
Supports up to 6-levels depth nested relations expansion. `),ve=s("br"),$e=k(`
|
||||
The expanded relations will be appended to the record under the
|
||||
`),ae=s("code"),ae.textContent="expand",ge=k(" property (eg. "),se=s("code"),se.textContent='"expand": {"relField1": {...}, ...}',ye=k(`).
|
||||
`),Se=s("br"),we=k(`
|
||||
Only the relations to which the request user has permissions to `),ne=s("strong"),ne.textContent="view",Te=k(" will be expanded."),Ce=p(),J(M.$$.fragment),re=p(),H=s("div"),H.textContent="Responses",ie=p(),P=s("div"),O=s("div");for(let e=0;e<y.length;e+=1)y[e].c();Re=p(),E=s("div");for(let e=0;e<g.length;e+=1)g[e].c();b(l,"class","m-b-sm"),b(f,"class","content txt-lg m-b-sm"),b(B,"class","m-b-xs"),b(z,"class","label label-primary"),b(Q,"class","content"),b(j,"class","txt-hint txt-sm txt-right"),b(S,"class","alert alert-success"),b(D,"class","section-title"),b(T,"class","table-compact table-border m-b-base"),b(H,"class","section-title"),b(O,"class","tabs-header compact left"),b(E,"class","tabs-content"),b(P,"class","tabs")},m(e,t){d(e,l,t),o(l,a),o(l,m),o(l,_),d(e,i,t),d(e,f,t),d(e,v,t),K($,e,t),d(e,w,t),d(e,B,t),d(e,G,t),d(e,S,t),o(S,z),o(S,de),o(S,Q),o(Q,q),o(q,ue),o(q,X),o(X,Y),o(q,pe),o(S,fe),o(S,j),d(e,Z,t),d(e,D,t),d(e,ee,t),d(e,T,t),o(T,te),o(T,he),o(T,F),o(F,C),o(C,oe),o(C,be),o(C,le),o(C,me),o(C,h),o(h,_e),K(R,h,null),o(h,ke),o(h,ve),o(h,$e),o(h,ae),o(h,ge),o(h,se),o(h,ye),o(h,Se),o(h,we),o(h,ne),o(h,Te),o(F,Ce),K(M,F,null),d(e,re,t),d(e,H,t),d(e,ie,t),d(e,P,t),o(P,O);for(let c=0;c<y.length;c+=1)y[c]&&y[c].m(O,null);o(P,Re),o(P,E);for(let c=0;c<g.length;c+=1)g[c]&&g[c].m(E,null);A=!0},p(e,[t]){var Fe,He;(!A||t&1)&&n!==(n=e[0].name+"")&&ce(m,n);const c={};t&9&&(c.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
const authData = await pb.collection('${(Fe=e[0])==null?void 0:Fe.name}').authRefresh();
|
||||
|
||||
// after the above you can also access the refreshed auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
`),t&9&&(c.dart=`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
final authData = await pb.collection('${(He=e[0])==null?void 0:He.name}').authRefresh();
|
||||
|
||||
// after the above you can also access the refreshed auth data from the authStore
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.model.id);
|
||||
`),$.$set(c),(!A||t&1)&&U!==(U=e[0].name+"")&&ce(Y,U),t&6&&(x=e[2],y=Oe(y,t,Ae,1,e,x,Pe,O,Je,Ve,null,Ne)),t&6&&(L=e[2],Ke(),g=Oe(g,t,Be,1,e,L,Me,E,Ie,ze,null,Le),We())},i(e){if(!A){N($.$$.fragment,e),N(R.$$.fragment,e),N(M.$$.fragment,e);for(let t=0;t<L.length;t+=1)N(g[t]);A=!0}},o(e){V($.$$.fragment,e),V(R.$$.fragment,e),V(M.$$.fragment,e);for(let t=0;t<g.length;t+=1)V(g[t]);A=!1},d(e){e&&u(l),e&&u(i),e&&u(f),e&&u(v),I($,e),e&&u(w),e&&u(B),e&&u(G),e&&u(S),e&&u(Z),e&&u(D),e&&u(ee),e&&u(T),I(R),I(M),e&&u(re),e&&u(H),e&&u(ie),e&&u(P);for(let t=0;t<y.length;t+=1)y[t].d();for(let t=0;t<g.length;t+=1)g[t].d()}}}function ot(r,l,a){let n,{collection:m=new Ge}=l,_=200,i=[];const f=v=>a(1,_=v.code);return r.$$set=v=>{"collection"in v&&a(0,m=v.collection)},r.$$.update=()=>{r.$$.dirty&1&&a(2,i=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:Ee.dummyCollectionRecord(m)},null,2)},{code:401,body:`
|
||||
{
|
||||
"code": 401,
|
||||
"message": "The request requires valid record authorization token to be set.",
|
||||
"data": {}
|
||||
}
|
||||
`},{code:403,body:`
|
||||
{
|
||||
"code": 403,
|
||||
"message": "The authorized record model is not allowed to perform this action.",
|
||||
"data": {}
|
||||
}
|
||||
`},{code:404,body:`
|
||||
{
|
||||
"code": 404,
|
||||
"message": "Missing auth record context.",
|
||||
"data": {}
|
||||
}
|
||||
`}])},a(3,n=Ee.getApiExampleUrl(Xe.baseUrl)),[m,_,i,n,f]}class nt extends Ue{constructor(l){super(),je(this,l,ot,tt,xe,{collection:0})}}export{nt as default};
|
||||
Vendored
+12
-12
@@ -1,4 +1,4 @@
|
||||
import{S as je,i as Fe,s as Ve,M as He,e as s,w as g,b as h,c as re,f as p,g as r,h as a,m as ce,x as ue,N as Pe,O as Le,k as Ee,P as Je,n as Ne,t as E,a as J,o as c,d as de,T as ze,C as Re,p as Ie,r as N,u as Ke}from"./index-79bb649c.js";import{S as Qe}from"./SdkTabs-22bd33d6.js";function xe(i,l,o){const n=i.slice();return n[5]=l[o],n}function We(i,l,o){const n=i.slice();return n[5]=l[o],n}function Ue(i,l){let o,n=l[5].code+"",m,k,u,b;function _(){return l[4](l[5])}return{key:i,first:null,c(){o=s("button"),m=g(n),k=h(),p(o,"class","tab-item"),N(o,"active",l[1]===l[5].code),this.first=o},m(v,A){r(v,o,A),a(o,m),a(o,k),u||(b=Ke(o,"click",_),u=!0)},p(v,A){l=v,A&4&&n!==(n=l[5].code+"")&&ue(m,n),A&6&&N(o,"active",l[1]===l[5].code)},d(v){v&&c(o),u=!1,b()}}}function Be(i,l){let o,n,m,k;return n=new He({props:{content:l[5].body}}),{key:i,first:null,c(){o=s("div"),re(n.$$.fragment),m=h(),p(o,"class","tab-item"),N(o,"active",l[1]===l[5].code),this.first=o},m(u,b){r(u,o,b),ce(n,o,null),a(o,m),k=!0},p(u,b){l=u;const _={};b&4&&(_.content=l[5].body),n.$set(_),(!k||b&6)&&N(o,"active",l[1]===l[5].code)},i(u){k||(E(n.$$.fragment,u),k=!0)},o(u){J(n.$$.fragment,u),k=!1},d(u){u&&c(o),de(n)}}}function Ge(i){let l,o,n=i[0].name+"",m,k,u,b,_,v,A,D,z,S,j,he,F,M,pe,I,V=i[0].name+"",K,be,Q,P,G,R,X,x,Y,y,Z,fe,ee,$,te,me,ae,ke,f,ge,C,_e,ve,we,le,Oe,oe,Ae,Se,ye,se,$e,ne,W,ie,T,U,O=[],Te=new Map,Ce,B,w=[],qe=new Map,q;v=new Qe({props:{js:`
|
||||
import{S as Ve,i as Le,s as Ee,M as je,e as s,w as k,b as h,c as z,f as p,g as r,h as a,m as I,x as he,N as xe,P as Je,k as Ne,Q as Qe,n as ze,t as V,a as L,o as c,d as K,T as Ie,C as We,p as Ke,r as G,u as Ge}from"./index-6e04d404.js";import{S as Xe}from"./SdkTabs-1a82fff4.js";import{F as Ye}from"./FieldsQueryParam-db646c6c.js";function Ue(i,l,o){const n=i.slice();return n[5]=l[o],n}function Be(i,l,o){const n=i.slice();return n[5]=l[o],n}function Fe(i,l){let o,n=l[5].code+"",m,g,u,b;function _(){return l[4](l[5])}return{key:i,first:null,c(){o=s("button"),m=k(n),g=h(),p(o,"class","tab-item"),G(o,"active",l[1]===l[5].code),this.first=o},m(v,A){r(v,o,A),a(o,m),a(o,g),u||(b=Ge(o,"click",_),u=!0)},p(v,A){l=v,A&4&&n!==(n=l[5].code+"")&&he(m,n),A&6&&G(o,"active",l[1]===l[5].code)},d(v){v&&c(o),u=!1,b()}}}function He(i,l){let o,n,m,g;return n=new je({props:{content:l[5].body}}),{key:i,first:null,c(){o=s("div"),z(n.$$.fragment),m=h(),p(o,"class","tab-item"),G(o,"active",l[1]===l[5].code),this.first=o},m(u,b){r(u,o,b),I(n,o,null),a(o,m),g=!0},p(u,b){l=u;const _={};b&4&&(_.content=l[5].body),n.$set(_),(!g||b&6)&&G(o,"active",l[1]===l[5].code)},i(u){g||(V(n.$$.fragment,u),g=!0)},o(u){L(n.$$.fragment,u),g=!1},d(u){u&&c(o),K(n)}}}function Ze(i){let l,o,n=i[0].name+"",m,g,u,b,_,v,A,P,X,S,E,pe,J,M,be,Y,N=i[0].name+"",Z,fe,ee,R,te,x,ae,W,le,y,oe,me,U,$,se,ge,ne,ke,f,_e,C,ve,we,Oe,ie,Ae,re,Se,ye,$e,ce,Te,Ce,q,ue,B,de,T,F,O=[],qe=new Map,De,H,w=[],Pe=new Map,D;v=new Xe({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${i[3]}');
|
||||
@@ -45,10 +45,10 @@ import{S as je,i as Fe,s as Ve,M as He,e as s,w as g,b as h,c as re,f as p,g as
|
||||
|
||||
// "logout" the last authenticated model
|
||||
pb.authStore.clear();
|
||||
`}}),C=new He({props:{content:"?expand=relField1,relField2.subRelField"}});let L=i[2];const De=e=>e[5].code;for(let e=0;e<L.length;e+=1){let t=We(i,L,e),d=De(t);Te.set(d,O[e]=Ue(d,t))}let H=i[2];const Me=e=>e[5].code;for(let e=0;e<H.length;e+=1){let t=xe(i,H,e),d=Me(t);qe.set(d,w[e]=Be(d,t))}return{c(){l=s("h3"),o=g("Auth with OAuth2 ("),m=g(n),k=g(")"),u=h(),b=s("div"),b.innerHTML=`<p>Authenticate with an OAuth2 provider and returns a new auth token and record data.</p>
|
||||
`}}),C=new je({props:{content:"?expand=relField1,relField2.subRelField"}}),q=new Ye({});let Q=i[2];const Me=e=>e[5].code;for(let e=0;e<Q.length;e+=1){let t=Be(i,Q,e),d=Me(t);qe.set(d,O[e]=Fe(d,t))}let j=i[2];const Re=e=>e[5].code;for(let e=0;e<j.length;e+=1){let t=Ue(i,j,e),d=Re(t);Pe.set(d,w[e]=He(d,t))}return{c(){l=s("h3"),o=k("Auth with OAuth2 ("),m=k(n),g=k(")"),u=h(),b=s("div"),b.innerHTML=`<p>Authenticate with an OAuth2 provider and returns a new auth token and record data.</p>
|
||||
<p>For more details please check the
|
||||
<a href="https://pocketbase.io/docs/authentication/#oauth2-integration" target="_blank" rel="noopener noreferrer">OAuth2 integration documentation
|
||||
</a>.</p>`,_=h(),re(v.$$.fragment),A=h(),D=s("h6"),D.textContent="API details",z=h(),S=s("div"),j=s("strong"),j.textContent="POST",he=h(),F=s("div"),M=s("p"),pe=g("/api/collections/"),I=s("strong"),K=g(V),be=g("/auth-with-oauth2"),Q=h(),P=s("div"),P.textContent="Body Parameters",G=h(),R=s("table"),R.innerHTML=`<thead><tr><th>Param</th>
|
||||
</a>.</p>`,_=h(),z(v.$$.fragment),A=h(),P=s("h6"),P.textContent="API details",X=h(),S=s("div"),E=s("strong"),E.textContent="POST",pe=h(),J=s("div"),M=s("p"),be=k("/api/collections/"),Y=s("strong"),Z=k(N),fe=k("/auth-with-oauth2"),ee=h(),R=s("div"),R.textContent="Body Parameters",te=h(),x=s("table"),x.innerHTML=`<thead><tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th></tr></thead>
|
||||
<tbody><tr><td><div class="inline-flex"><span class="label label-success">Required</span>
|
||||
@@ -75,15 +75,15 @@ import{S as je,i as Fe,s as Ve,M as He,e as s,w as g,b as h,c as re,f as p,g as
|
||||
regular <strong>create</strong> action.
|
||||
<br/>
|
||||
<em>The data can only be in <code>json</code>, aka. <code>multipart/form-data</code> and files
|
||||
upload currently are not supported during OAuth2 sign-ups.</em></p></td></tr></tbody>`,X=h(),x=s("div"),x.textContent="Query parameters",Y=h(),y=s("table"),Z=s("thead"),Z.innerHTML=`<tr><th>Param</th>
|
||||
upload currently are not supported during OAuth2 sign-ups.</em></p></td></tr></tbody>`,ae=h(),W=s("div"),W.textContent="Query parameters",le=h(),y=s("table"),oe=s("thead"),oe.innerHTML=`<tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th></tr>`,fe=h(),ee=s("tbody"),$=s("tr"),te=s("td"),te.textContent="expand",me=h(),ae=s("td"),ae.innerHTML='<span class="label">String</span>',ke=h(),f=s("td"),ge=g(`Auto expand record relations. Ex.:
|
||||
`),re(C.$$.fragment),_e=g(`
|
||||
Supports up to 6-levels depth nested relations expansion. `),ve=s("br"),we=g(`
|
||||
<th width="60%">Description</th></tr>`,me=h(),U=s("tbody"),$=s("tr"),se=s("td"),se.textContent="expand",ge=h(),ne=s("td"),ne.innerHTML='<span class="label">String</span>',ke=h(),f=s("td"),_e=k(`Auto expand record relations. Ex.:
|
||||
`),z(C.$$.fragment),ve=k(`
|
||||
Supports up to 6-levels depth nested relations expansion. `),we=s("br"),Oe=k(`
|
||||
The expanded relations will be appended to the record under the
|
||||
`),le=s("code"),le.textContent="expand",Oe=g(" property (eg. "),oe=s("code"),oe.textContent='"expand": {"relField1": {...}, ...}',Ae=g(`).
|
||||
`),Se=s("br"),ye=g(`
|
||||
Only the relations to which the request user has permissions to `),se=s("strong"),se.textContent="view",$e=g(" will be expanded."),ne=h(),W=s("div"),W.textContent="Responses",ie=h(),T=s("div"),U=s("div");for(let e=0;e<O.length;e+=1)O[e].c();Ce=h(),B=s("div");for(let e=0;e<w.length;e+=1)w[e].c();p(l,"class","m-b-sm"),p(b,"class","content txt-lg m-b-sm"),p(D,"class","m-b-xs"),p(j,"class","label label-primary"),p(F,"class","content"),p(S,"class","alert alert-success"),p(P,"class","section-title"),p(R,"class","table-compact table-border m-b-base"),p(x,"class","section-title"),p(y,"class","table-compact table-border m-b-base"),p(W,"class","section-title"),p(U,"class","tabs-header compact left"),p(B,"class","tabs-content"),p(T,"class","tabs")},m(e,t){r(e,l,t),a(l,o),a(l,m),a(l,k),r(e,u,t),r(e,b,t),r(e,_,t),ce(v,e,t),r(e,A,t),r(e,D,t),r(e,z,t),r(e,S,t),a(S,j),a(S,he),a(S,F),a(F,M),a(M,pe),a(M,I),a(I,K),a(M,be),r(e,Q,t),r(e,P,t),r(e,G,t),r(e,R,t),r(e,X,t),r(e,x,t),r(e,Y,t),r(e,y,t),a(y,Z),a(y,fe),a(y,ee),a(ee,$),a($,te),a($,me),a($,ae),a($,ke),a($,f),a(f,ge),ce(C,f,null),a(f,_e),a(f,ve),a(f,we),a(f,le),a(f,Oe),a(f,oe),a(f,Ae),a(f,Se),a(f,ye),a(f,se),a(f,$e),r(e,ne,t),r(e,W,t),r(e,ie,t),r(e,T,t),a(T,U);for(let d=0;d<O.length;d+=1)O[d]&&O[d].m(U,null);a(T,Ce),a(T,B);for(let d=0;d<w.length;d+=1)w[d]&&w[d].m(B,null);q=!0},p(e,[t]){(!q||t&1)&&n!==(n=e[0].name+"")&&ue(m,n);const d={};t&8&&(d.js=`
|
||||
`),ie=s("code"),ie.textContent="expand",Ae=k(" property (eg. "),re=s("code"),re.textContent='"expand": {"relField1": {...}, ...}',Se=k(`).
|
||||
`),ye=s("br"),$e=k(`
|
||||
Only the relations to which the request user has permissions to `),ce=s("strong"),ce.textContent="view",Te=k(" will be expanded."),Ce=h(),z(q.$$.fragment),ue=h(),B=s("div"),B.textContent="Responses",de=h(),T=s("div"),F=s("div");for(let e=0;e<O.length;e+=1)O[e].c();De=h(),H=s("div");for(let e=0;e<w.length;e+=1)w[e].c();p(l,"class","m-b-sm"),p(b,"class","content txt-lg m-b-sm"),p(P,"class","m-b-xs"),p(E,"class","label label-primary"),p(J,"class","content"),p(S,"class","alert alert-success"),p(R,"class","section-title"),p(x,"class","table-compact table-border m-b-base"),p(W,"class","section-title"),p(y,"class","table-compact table-border m-b-base"),p(B,"class","section-title"),p(F,"class","tabs-header compact left"),p(H,"class","tabs-content"),p(T,"class","tabs")},m(e,t){r(e,l,t),a(l,o),a(l,m),a(l,g),r(e,u,t),r(e,b,t),r(e,_,t),I(v,e,t),r(e,A,t),r(e,P,t),r(e,X,t),r(e,S,t),a(S,E),a(S,pe),a(S,J),a(J,M),a(M,be),a(M,Y),a(Y,Z),a(M,fe),r(e,ee,t),r(e,R,t),r(e,te,t),r(e,x,t),r(e,ae,t),r(e,W,t),r(e,le,t),r(e,y,t),a(y,oe),a(y,me),a(y,U),a(U,$),a($,se),a($,ge),a($,ne),a($,ke),a($,f),a(f,_e),I(C,f,null),a(f,ve),a(f,we),a(f,Oe),a(f,ie),a(f,Ae),a(f,re),a(f,Se),a(f,ye),a(f,$e),a(f,ce),a(f,Te),a(U,Ce),I(q,U,null),r(e,ue,t),r(e,B,t),r(e,de,t),r(e,T,t),a(T,F);for(let d=0;d<O.length;d+=1)O[d]&&O[d].m(F,null);a(T,De),a(T,H);for(let d=0;d<w.length;d+=1)w[d]&&w[d].m(H,null);D=!0},p(e,[t]){(!D||t&1)&&n!==(n=e[0].name+"")&&he(m,n);const d={};t&8&&(d.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${e[3]}');
|
||||
@@ -130,7 +130,7 @@ import{S as je,i as Fe,s as Ve,M as He,e as s,w as g,b as h,c as re,f as p,g as
|
||||
|
||||
// "logout" the last authenticated model
|
||||
pb.authStore.clear();
|
||||
`),v.$set(d),(!q||t&1)&&V!==(V=e[0].name+"")&&ue(K,V),t&6&&(L=e[2],O=Pe(O,t,De,1,e,L,Te,U,Le,Ue,null,We)),t&6&&(H=e[2],Ee(),w=Pe(w,t,Me,1,e,H,qe,B,Je,Be,null,xe),Ne())},i(e){if(!q){E(v.$$.fragment,e),E(C.$$.fragment,e);for(let t=0;t<H.length;t+=1)E(w[t]);q=!0}},o(e){J(v.$$.fragment,e),J(C.$$.fragment,e);for(let t=0;t<w.length;t+=1)J(w[t]);q=!1},d(e){e&&c(l),e&&c(u),e&&c(b),e&&c(_),de(v,e),e&&c(A),e&&c(D),e&&c(z),e&&c(S),e&&c(Q),e&&c(P),e&&c(G),e&&c(R),e&&c(X),e&&c(x),e&&c(Y),e&&c(y),de(C),e&&c(ne),e&&c(W),e&&c(ie),e&&c(T);for(let t=0;t<O.length;t+=1)O[t].d();for(let t=0;t<w.length;t+=1)w[t].d()}}}function Xe(i,l,o){let n,{collection:m=new ze}=l,k=200,u=[];const b=_=>o(1,k=_.code);return i.$$set=_=>{"collection"in _&&o(0,m=_.collection)},i.$$.update=()=>{i.$$.dirty&1&&o(2,u=[{code:200,body:JSON.stringify({token:"JWT_AUTH_TOKEN",record:Re.dummyCollectionRecord(m),meta:{id:"abc123",name:"John Doe",username:"john.doe",email:"test@example.com",avatarUrl:"https://example.com/avatar.png",accessToken:"...",refreshToken:"...",rawUser:{}}},null,2)},{code:400,body:`
|
||||
`),v.$set(d),(!D||t&1)&&N!==(N=e[0].name+"")&&he(Z,N),t&6&&(Q=e[2],O=xe(O,t,Me,1,e,Q,qe,F,Je,Fe,null,Be)),t&6&&(j=e[2],Ne(),w=xe(w,t,Re,1,e,j,Pe,H,Qe,He,null,Ue),ze())},i(e){if(!D){V(v.$$.fragment,e),V(C.$$.fragment,e),V(q.$$.fragment,e);for(let t=0;t<j.length;t+=1)V(w[t]);D=!0}},o(e){L(v.$$.fragment,e),L(C.$$.fragment,e),L(q.$$.fragment,e);for(let t=0;t<w.length;t+=1)L(w[t]);D=!1},d(e){e&&c(l),e&&c(u),e&&c(b),e&&c(_),K(v,e),e&&c(A),e&&c(P),e&&c(X),e&&c(S),e&&c(ee),e&&c(R),e&&c(te),e&&c(x),e&&c(ae),e&&c(W),e&&c(le),e&&c(y),K(C),K(q),e&&c(ue),e&&c(B),e&&c(de),e&&c(T);for(let t=0;t<O.length;t+=1)O[t].d();for(let t=0;t<w.length;t+=1)w[t].d()}}}function et(i,l,o){let n,{collection:m=new Ie}=l,g=200,u=[];const b=_=>o(1,g=_.code);return i.$$set=_=>{"collection"in _&&o(0,m=_.collection)},i.$$.update=()=>{i.$$.dirty&1&&o(2,u=[{code:200,body:JSON.stringify({token:"JWT_AUTH_TOKEN",record:We.dummyCollectionRecord(m),meta:{id:"abc123",name:"John Doe",username:"john.doe",email:"test@example.com",avatarUrl:"https://example.com/avatar.png",accessToken:"...",refreshToken:"...",rawUser:{}}},null,2)},{code:400,body:`
|
||||
{
|
||||
"code": 400,
|
||||
"message": "An error occurred while submitting the form.",
|
||||
@@ -141,4 +141,4 @@ import{S as je,i as Fe,s as Ve,M as He,e as s,w as g,b as h,c as re,f as p,g as
|
||||
}
|
||||
}
|
||||
}
|
||||
`}])},o(3,n=Re.getApiExampleUrl(Ie.baseUrl)),[m,k,u,n,b]}class et extends je{constructor(l){super(),Fe(this,l,Xe,Ge,Ve,{collection:0})}}export{et as default};
|
||||
`}])},o(3,n=We.getApiExampleUrl(Ke.baseUrl)),[m,g,u,n,b]}class ot extends Ve{constructor(l){super(),Le(this,l,et,Ze,Ee,{collection:0})}}export{ot as default};
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import{S as ve,i as ye,s as ge,M as we,e as s,w as f,b as d,c as ot,f as h,g as r,h as e,m as st,x as Ut,N as ue,P as $e,k as Pe,Q as Re,n as Ce,t as Z,a as x,o as c,d as nt,T as Te,C as fe,p as Ae,r as it,u as Oe}from"./index-6e04d404.js";import{S as Me}from"./SdkTabs-1a82fff4.js";import{F as Ue}from"./FieldsQueryParam-db646c6c.js";function pe(n,l,a){const i=n.slice();return i[8]=l[a],i}function be(n,l,a){const i=n.slice();return i[8]=l[a],i}function De(n){let l;return{c(){l=f("email")},m(a,i){r(a,l,i)},d(a){a&&c(l)}}}function Ee(n){let l;return{c(){l=f("username")},m(a,i){r(a,l,i)},d(a){a&&c(l)}}}function We(n){let l;return{c(){l=f("username/email")},m(a,i){r(a,l,i)},d(a){a&&c(l)}}}function me(n){let l;return{c(){l=s("strong"),l.textContent="username"},m(a,i){r(a,l,i)},d(a){a&&c(l)}}}function he(n){let l;return{c(){l=f("or")},m(a,i){r(a,l,i)},d(a){a&&c(l)}}}function _e(n){let l;return{c(){l=s("strong"),l.textContent="email"},m(a,i){r(a,l,i)},d(a){a&&c(l)}}}function ke(n,l){let a,i=l[8].code+"",S,m,p,u;function _(){return l[7](l[8])}return{key:n,first:null,c(){a=s("button"),S=f(i),m=d(),h(a,"class","tab-item"),it(a,"active",l[3]===l[8].code),this.first=a},m(R,C){r(R,a,C),e(a,S),e(a,m),p||(u=Oe(a,"click",_),p=!0)},p(R,C){l=R,C&16&&i!==(i=l[8].code+"")&&Ut(S,i),C&24&&it(a,"active",l[3]===l[8].code)},d(R){R&&c(a),p=!1,u()}}}function Se(n,l){let a,i,S,m;return i=new we({props:{content:l[8].body}}),{key:n,first:null,c(){a=s("div"),ot(i.$$.fragment),S=d(),h(a,"class","tab-item"),it(a,"active",l[3]===l[8].code),this.first=a},m(p,u){r(p,a,u),st(i,a,null),e(a,S),m=!0},p(p,u){l=p;const _={};u&16&&(_.content=l[8].body),i.$set(_),(!m||u&24)&&it(a,"active",l[3]===l[8].code)},i(p){m||(Z(i.$$.fragment,p),m=!0)},o(p){x(i.$$.fragment,p),m=!1},d(p){p&&c(a),nt(i)}}}function Le(n){var ie,re;let l,a,i=n[0].name+"",S,m,p,u,_,R,C,T,B,Dt,rt,O,ct,N,dt,M,tt,Et,et,I,Wt,ut,lt=n[0].name+"",ft,Lt,pt,V,bt,U,mt,Bt,Q,D,ht,qt,_t,Ft,$,Ht,kt,St,wt,Yt,vt,yt,j,gt,E,$t,Nt,J,W,Pt,It,Rt,Vt,k,Qt,q,jt,Jt,Kt,Ct,zt,Tt,Gt,Xt,Zt,At,xt,te,F,Ot,K,Mt,L,z,A=[],ee=new Map,le,G,w=[],ae=new Map,H;function oe(t,o){if(t[1]&&t[2])return We;if(t[1])return Ee;if(t[2])return De}let Y=oe(n),P=Y&&Y(n);O=new Me({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${n[6]}');
|
||||
|
||||
...
|
||||
|
||||
const authData = await pb.collection('${(ie=n[0])==null?void 0:ie.name}').authWithPassword(
|
||||
'${n[5]}',
|
||||
'YOUR_PASSWORD',
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
|
||||
// "logout" the last authenticated account
|
||||
pb.authStore.clear();
|
||||
`,dart:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${n[6]}');
|
||||
|
||||
...
|
||||
|
||||
final authData = await pb.collection('${(re=n[0])==null?void 0:re.name}').authWithPassword(
|
||||
'${n[5]}',
|
||||
'YOUR_PASSWORD',
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.model.id);
|
||||
|
||||
// "logout" the last authenticated account
|
||||
pb.authStore.clear();
|
||||
`}});let v=n[1]&&me(),y=n[1]&&n[2]&&he(),g=n[2]&&_e();q=new we({props:{content:"?expand=relField1,relField2.subRelField"}}),F=new Ue({});let at=n[4];const se=t=>t[8].code;for(let t=0;t<at.length;t+=1){let o=be(n,at,t),b=se(o);ee.set(b,A[t]=ke(b,o))}let X=n[4];const ne=t=>t[8].code;for(let t=0;t<X.length;t+=1){let o=pe(n,X,t),b=ne(o);ae.set(b,w[t]=Se(b,o))}return{c(){l=s("h3"),a=f("Auth with password ("),S=f(i),m=f(")"),p=d(),u=s("div"),_=s("p"),R=f(`Returns new auth token and account data by a combination of
|
||||
`),C=s("strong"),P&&P.c(),T=f(`
|
||||
and `),B=s("strong"),B.textContent="password",Dt=f("."),rt=d(),ot(O.$$.fragment),ct=d(),N=s("h6"),N.textContent="API details",dt=d(),M=s("div"),tt=s("strong"),tt.textContent="POST",Et=d(),et=s("div"),I=s("p"),Wt=f("/api/collections/"),ut=s("strong"),ft=f(lt),Lt=f("/auth-with-password"),pt=d(),V=s("div"),V.textContent="Body Parameters",bt=d(),U=s("table"),mt=s("thead"),mt.innerHTML=`<tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th></tr>`,Bt=d(),Q=s("tbody"),D=s("tr"),ht=s("td"),ht.innerHTML=`<div class="inline-flex"><span class="label label-success">Required</span>
|
||||
<span>identity</span></div>`,qt=d(),_t=s("td"),_t.innerHTML='<span class="label">String</span>',Ft=d(),$=s("td"),Ht=f(`The
|
||||
`),v&&v.c(),kt=d(),y&&y.c(),St=d(),g&&g.c(),wt=f(`
|
||||
of the record to authenticate.`),Yt=d(),vt=s("tr"),vt.innerHTML=`<td><div class="inline-flex"><span class="label label-success">Required</span>
|
||||
<span>password</span></div></td>
|
||||
<td><span class="label">String</span></td>
|
||||
<td>The auth record password.</td>`,yt=d(),j=s("div"),j.textContent="Query parameters",gt=d(),E=s("table"),$t=s("thead"),$t.innerHTML=`<tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th></tr>`,Nt=d(),J=s("tbody"),W=s("tr"),Pt=s("td"),Pt.textContent="expand",It=d(),Rt=s("td"),Rt.innerHTML='<span class="label">String</span>',Vt=d(),k=s("td"),Qt=f(`Auto expand record relations. Ex.:
|
||||
`),ot(q.$$.fragment),jt=f(`
|
||||
Supports up to 6-levels depth nested relations expansion. `),Jt=s("br"),Kt=f(`
|
||||
The expanded relations will be appended to the record under the
|
||||
`),Ct=s("code"),Ct.textContent="expand",zt=f(" property (eg. "),Tt=s("code"),Tt.textContent='"expand": {"relField1": {...}, ...}',Gt=f(`).
|
||||
`),Xt=s("br"),Zt=f(`
|
||||
Only the relations to which the request user has permissions to `),At=s("strong"),At.textContent="view",xt=f(" will be expanded."),te=d(),ot(F.$$.fragment),Ot=d(),K=s("div"),K.textContent="Responses",Mt=d(),L=s("div"),z=s("div");for(let t=0;t<A.length;t+=1)A[t].c();le=d(),G=s("div");for(let t=0;t<w.length;t+=1)w[t].c();h(l,"class","m-b-sm"),h(u,"class","content txt-lg m-b-sm"),h(N,"class","m-b-xs"),h(tt,"class","label label-primary"),h(et,"class","content"),h(M,"class","alert alert-success"),h(V,"class","section-title"),h(U,"class","table-compact table-border m-b-base"),h(j,"class","section-title"),h(E,"class","table-compact table-border m-b-base"),h(K,"class","section-title"),h(z,"class","tabs-header compact left"),h(G,"class","tabs-content"),h(L,"class","tabs")},m(t,o){r(t,l,o),e(l,a),e(l,S),e(l,m),r(t,p,o),r(t,u,o),e(u,_),e(_,R),e(_,C),P&&P.m(C,null),e(_,T),e(_,B),e(_,Dt),r(t,rt,o),st(O,t,o),r(t,ct,o),r(t,N,o),r(t,dt,o),r(t,M,o),e(M,tt),e(M,Et),e(M,et),e(et,I),e(I,Wt),e(I,ut),e(ut,ft),e(I,Lt),r(t,pt,o),r(t,V,o),r(t,bt,o),r(t,U,o),e(U,mt),e(U,Bt),e(U,Q),e(Q,D),e(D,ht),e(D,qt),e(D,_t),e(D,Ft),e(D,$),e($,Ht),v&&v.m($,null),e($,kt),y&&y.m($,null),e($,St),g&&g.m($,null),e($,wt),e(Q,Yt),e(Q,vt),r(t,yt,o),r(t,j,o),r(t,gt,o),r(t,E,o),e(E,$t),e(E,Nt),e(E,J),e(J,W),e(W,Pt),e(W,It),e(W,Rt),e(W,Vt),e(W,k),e(k,Qt),st(q,k,null),e(k,jt),e(k,Jt),e(k,Kt),e(k,Ct),e(k,zt),e(k,Tt),e(k,Gt),e(k,Xt),e(k,Zt),e(k,At),e(k,xt),e(J,te),st(F,J,null),r(t,Ot,o),r(t,K,o),r(t,Mt,o),r(t,L,o),e(L,z);for(let b=0;b<A.length;b+=1)A[b]&&A[b].m(z,null);e(L,le),e(L,G);for(let b=0;b<w.length;b+=1)w[b]&&w[b].m(G,null);H=!0},p(t,[o]){var ce,de;(!H||o&1)&&i!==(i=t[0].name+"")&&Ut(S,i),Y!==(Y=oe(t))&&(P&&P.d(1),P=Y&&Y(t),P&&(P.c(),P.m(C,null)));const b={};o&97&&(b.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${t[6]}');
|
||||
|
||||
...
|
||||
|
||||
const authData = await pb.collection('${(ce=t[0])==null?void 0:ce.name}').authWithPassword(
|
||||
'${t[5]}',
|
||||
'YOUR_PASSWORD',
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
|
||||
// "logout" the last authenticated account
|
||||
pb.authStore.clear();
|
||||
`),o&97&&(b.dart=`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${t[6]}');
|
||||
|
||||
...
|
||||
|
||||
final authData = await pb.collection('${(de=t[0])==null?void 0:de.name}').authWithPassword(
|
||||
'${t[5]}',
|
||||
'YOUR_PASSWORD',
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.model.id);
|
||||
|
||||
// "logout" the last authenticated account
|
||||
pb.authStore.clear();
|
||||
`),O.$set(b),(!H||o&1)&<!==(lt=t[0].name+"")&&Ut(ft,lt),t[1]?v||(v=me(),v.c(),v.m($,kt)):v&&(v.d(1),v=null),t[1]&&t[2]?y||(y=he(),y.c(),y.m($,St)):y&&(y.d(1),y=null),t[2]?g||(g=_e(),g.c(),g.m($,wt)):g&&(g.d(1),g=null),o&24&&(at=t[4],A=ue(A,o,se,1,t,at,ee,z,$e,ke,null,be)),o&24&&(X=t[4],Pe(),w=ue(w,o,ne,1,t,X,ae,G,Re,Se,null,pe),Ce())},i(t){if(!H){Z(O.$$.fragment,t),Z(q.$$.fragment,t),Z(F.$$.fragment,t);for(let o=0;o<X.length;o+=1)Z(w[o]);H=!0}},o(t){x(O.$$.fragment,t),x(q.$$.fragment,t),x(F.$$.fragment,t);for(let o=0;o<w.length;o+=1)x(w[o]);H=!1},d(t){t&&c(l),t&&c(p),t&&c(u),P&&P.d(),t&&c(rt),nt(O,t),t&&c(ct),t&&c(N),t&&c(dt),t&&c(M),t&&c(pt),t&&c(V),t&&c(bt),t&&c(U),v&&v.d(),y&&y.d(),g&&g.d(),t&&c(yt),t&&c(j),t&&c(gt),t&&c(E),nt(q),nt(F),t&&c(Ot),t&&c(K),t&&c(Mt),t&&c(L);for(let o=0;o<A.length;o+=1)A[o].d();for(let o=0;o<w.length;o+=1)w[o].d()}}}function Be(n,l,a){let i,S,m,p,{collection:u=new Te}=l,_=200,R=[];const C=T=>a(3,_=T.code);return n.$$set=T=>{"collection"in T&&a(0,u=T.collection)},n.$$.update=()=>{var T,B;n.$$.dirty&1&&a(2,S=(T=u==null?void 0:u.options)==null?void 0:T.allowEmailAuth),n.$$.dirty&1&&a(1,m=(B=u==null?void 0:u.options)==null?void 0:B.allowUsernameAuth),n.$$.dirty&6&&a(5,p=m&&S?"YOUR_USERNAME_OR_EMAIL":m?"YOUR_USERNAME":"YOUR_EMAIL"),n.$$.dirty&1&&a(4,R=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:fe.dummyCollectionRecord(u)},null,2)},{code:400,body:`
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to authenticate.",
|
||||
"data": {
|
||||
"identity": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`}])},a(6,i=fe.getApiExampleUrl(Ae.baseUrl)),[u,m,S,_,R,p,i,C]}class Ye extends ve{constructor(l){super(),ye(this,l,Be,Le,ge,{collection:0})}}export{Ye as default};
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
import{S as Se,i as ve,s as we,M as ke,e as s,w as f,b as u,c as Ot,f as h,g as r,h as o,m as Tt,x as At,N as ce,O as ye,k as ge,P as Pe,n as $e,t as tt,a as et,o as c,d as Mt,T as Re,C as de,p as Ce,r as lt,u as Oe}from"./index-79bb649c.js";import{S as Te}from"./SdkTabs-22bd33d6.js";function ue(n,e,l){const i=n.slice();return i[8]=e[l],i}function fe(n,e,l){const i=n.slice();return i[8]=e[l],i}function Ae(n){let e;return{c(){e=f("email")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function Me(n){let e;return{c(){e=f("username")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function Ue(n){let e;return{c(){e=f("username/email")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function pe(n){let e;return{c(){e=s("strong"),e.textContent="username"},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function be(n){let e;return{c(){e=f("or")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function me(n){let e;return{c(){e=s("strong"),e.textContent="email"},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function he(n,e){let l,i=e[8].code+"",S,m,p,d;function _(){return e[7](e[8])}return{key:n,first:null,c(){l=s("button"),S=f(i),m=u(),h(l,"class","tab-item"),lt(l,"active",e[3]===e[8].code),this.first=l},m(R,C){r(R,l,C),o(l,S),o(l,m),p||(d=Oe(l,"click",_),p=!0)},p(R,C){e=R,C&16&&i!==(i=e[8].code+"")&&At(S,i),C&24&<(l,"active",e[3]===e[8].code)},d(R){R&&c(l),p=!1,d()}}}function _e(n,e){let l,i,S,m;return i=new ke({props:{content:e[8].body}}),{key:n,first:null,c(){l=s("div"),Ot(i.$$.fragment),S=u(),h(l,"class","tab-item"),lt(l,"active",e[3]===e[8].code),this.first=l},m(p,d){r(p,l,d),Tt(i,l,null),o(l,S),m=!0},p(p,d){e=p;const _={};d&16&&(_.content=e[8].body),i.$set(_),(!m||d&24)&<(l,"active",e[3]===e[8].code)},i(p){m||(tt(i.$$.fragment,p),m=!0)},o(p){et(i.$$.fragment,p),m=!1},d(p){p&&c(l),Mt(i)}}}function De(n){var se,ne;let e,l,i=n[0].name+"",S,m,p,d,_,R,C,O,B,Ut,ot,A,at,F,st,M,G,Dt,X,N,Et,nt,Z=n[0].name+"",it,Wt,rt,I,ct,U,dt,Lt,V,D,ut,Bt,ft,Ht,P,Yt,pt,bt,mt,qt,ht,_t,j,kt,E,St,Ft,vt,W,wt,Nt,yt,It,k,Vt,H,jt,Jt,Kt,gt,Qt,Pt,zt,Gt,Xt,$t,Zt,Rt,J,Ct,L,K,T=[],xt=new Map,te,Q,v=[],ee=new Map,Y;function le(t,a){if(t[1]&&t[2])return Ue;if(t[1])return Me;if(t[2])return Ae}let q=le(n),$=q&&q(n);A=new Te({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${n[6]}');
|
||||
|
||||
...
|
||||
|
||||
const authData = await pb.collection('${(se=n[0])==null?void 0:se.name}').authWithPassword(
|
||||
'${n[5]}',
|
||||
'YOUR_PASSWORD',
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
|
||||
// "logout" the last authenticated account
|
||||
pb.authStore.clear();
|
||||
`,dart:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${n[6]}');
|
||||
|
||||
...
|
||||
|
||||
final authData = await pb.collection('${(ne=n[0])==null?void 0:ne.name}').authWithPassword(
|
||||
'${n[5]}',
|
||||
'YOUR_PASSWORD',
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.model.id);
|
||||
|
||||
// "logout" the last authenticated account
|
||||
pb.authStore.clear();
|
||||
`}});let w=n[1]&&pe(),y=n[1]&&n[2]&&be(),g=n[2]&&me();H=new ke({props:{content:"?expand=relField1,relField2.subRelField"}});let x=n[4];const oe=t=>t[8].code;for(let t=0;t<x.length;t+=1){let a=fe(n,x,t),b=oe(a);xt.set(b,T[t]=he(b,a))}let z=n[4];const ae=t=>t[8].code;for(let t=0;t<z.length;t+=1){let a=ue(n,z,t),b=ae(a);ee.set(b,v[t]=_e(b,a))}return{c(){e=s("h3"),l=f("Auth with password ("),S=f(i),m=f(")"),p=u(),d=s("div"),_=s("p"),R=f(`Returns new auth token and account data by a combination of
|
||||
`),C=s("strong"),$&&$.c(),O=f(`
|
||||
and `),B=s("strong"),B.textContent="password",Ut=f("."),ot=u(),Ot(A.$$.fragment),at=u(),F=s("h6"),F.textContent="API details",st=u(),M=s("div"),G=s("strong"),G.textContent="POST",Dt=u(),X=s("div"),N=s("p"),Et=f("/api/collections/"),nt=s("strong"),it=f(Z),Wt=f("/auth-with-password"),rt=u(),I=s("div"),I.textContent="Body Parameters",ct=u(),U=s("table"),dt=s("thead"),dt.innerHTML=`<tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th></tr>`,Lt=u(),V=s("tbody"),D=s("tr"),ut=s("td"),ut.innerHTML=`<div class="inline-flex"><span class="label label-success">Required</span>
|
||||
<span>identity</span></div>`,Bt=u(),ft=s("td"),ft.innerHTML='<span class="label">String</span>',Ht=u(),P=s("td"),Yt=f(`The
|
||||
`),w&&w.c(),pt=u(),y&&y.c(),bt=u(),g&&g.c(),mt=f(`
|
||||
of the record to authenticate.`),qt=u(),ht=s("tr"),ht.innerHTML=`<td><div class="inline-flex"><span class="label label-success">Required</span>
|
||||
<span>password</span></div></td>
|
||||
<td><span class="label">String</span></td>
|
||||
<td>The auth record password.</td>`,_t=u(),j=s("div"),j.textContent="Query parameters",kt=u(),E=s("table"),St=s("thead"),St.innerHTML=`<tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th></tr>`,Ft=u(),vt=s("tbody"),W=s("tr"),wt=s("td"),wt.textContent="expand",Nt=u(),yt=s("td"),yt.innerHTML='<span class="label">String</span>',It=u(),k=s("td"),Vt=f(`Auto expand record relations. Ex.:
|
||||
`),Ot(H.$$.fragment),jt=f(`
|
||||
Supports up to 6-levels depth nested relations expansion. `),Jt=s("br"),Kt=f(`
|
||||
The expanded relations will be appended to the record under the
|
||||
`),gt=s("code"),gt.textContent="expand",Qt=f(" property (eg. "),Pt=s("code"),Pt.textContent='"expand": {"relField1": {...}, ...}',zt=f(`).
|
||||
`),Gt=s("br"),Xt=f(`
|
||||
Only the relations to which the request user has permissions to `),$t=s("strong"),$t.textContent="view",Zt=f(" will be expanded."),Rt=u(),J=s("div"),J.textContent="Responses",Ct=u(),L=s("div"),K=s("div");for(let t=0;t<T.length;t+=1)T[t].c();te=u(),Q=s("div");for(let t=0;t<v.length;t+=1)v[t].c();h(e,"class","m-b-sm"),h(d,"class","content txt-lg m-b-sm"),h(F,"class","m-b-xs"),h(G,"class","label label-primary"),h(X,"class","content"),h(M,"class","alert alert-success"),h(I,"class","section-title"),h(U,"class","table-compact table-border m-b-base"),h(j,"class","section-title"),h(E,"class","table-compact table-border m-b-base"),h(J,"class","section-title"),h(K,"class","tabs-header compact left"),h(Q,"class","tabs-content"),h(L,"class","tabs")},m(t,a){r(t,e,a),o(e,l),o(e,S),o(e,m),r(t,p,a),r(t,d,a),o(d,_),o(_,R),o(_,C),$&&$.m(C,null),o(_,O),o(_,B),o(_,Ut),r(t,ot,a),Tt(A,t,a),r(t,at,a),r(t,F,a),r(t,st,a),r(t,M,a),o(M,G),o(M,Dt),o(M,X),o(X,N),o(N,Et),o(N,nt),o(nt,it),o(N,Wt),r(t,rt,a),r(t,I,a),r(t,ct,a),r(t,U,a),o(U,dt),o(U,Lt),o(U,V),o(V,D),o(D,ut),o(D,Bt),o(D,ft),o(D,Ht),o(D,P),o(P,Yt),w&&w.m(P,null),o(P,pt),y&&y.m(P,null),o(P,bt),g&&g.m(P,null),o(P,mt),o(V,qt),o(V,ht),r(t,_t,a),r(t,j,a),r(t,kt,a),r(t,E,a),o(E,St),o(E,Ft),o(E,vt),o(vt,W),o(W,wt),o(W,Nt),o(W,yt),o(W,It),o(W,k),o(k,Vt),Tt(H,k,null),o(k,jt),o(k,Jt),o(k,Kt),o(k,gt),o(k,Qt),o(k,Pt),o(k,zt),o(k,Gt),o(k,Xt),o(k,$t),o(k,Zt),r(t,Rt,a),r(t,J,a),r(t,Ct,a),r(t,L,a),o(L,K);for(let b=0;b<T.length;b+=1)T[b]&&T[b].m(K,null);o(L,te),o(L,Q);for(let b=0;b<v.length;b+=1)v[b]&&v[b].m(Q,null);Y=!0},p(t,[a]){var ie,re;(!Y||a&1)&&i!==(i=t[0].name+"")&&At(S,i),q!==(q=le(t))&&($&&$.d(1),$=q&&q(t),$&&($.c(),$.m(C,null)));const b={};a&97&&(b.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${t[6]}');
|
||||
|
||||
...
|
||||
|
||||
const authData = await pb.collection('${(ie=t[0])==null?void 0:ie.name}').authWithPassword(
|
||||
'${t[5]}',
|
||||
'YOUR_PASSWORD',
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
|
||||
// "logout" the last authenticated account
|
||||
pb.authStore.clear();
|
||||
`),a&97&&(b.dart=`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${t[6]}');
|
||||
|
||||
...
|
||||
|
||||
final authData = await pb.collection('${(re=t[0])==null?void 0:re.name}').authWithPassword(
|
||||
'${t[5]}',
|
||||
'YOUR_PASSWORD',
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.model.id);
|
||||
|
||||
// "logout" the last authenticated account
|
||||
pb.authStore.clear();
|
||||
`),A.$set(b),(!Y||a&1)&&Z!==(Z=t[0].name+"")&&At(it,Z),t[1]?w||(w=pe(),w.c(),w.m(P,pt)):w&&(w.d(1),w=null),t[1]&&t[2]?y||(y=be(),y.c(),y.m(P,bt)):y&&(y.d(1),y=null),t[2]?g||(g=me(),g.c(),g.m(P,mt)):g&&(g.d(1),g=null),a&24&&(x=t[4],T=ce(T,a,oe,1,t,x,xt,K,ye,he,null,fe)),a&24&&(z=t[4],ge(),v=ce(v,a,ae,1,t,z,ee,Q,Pe,_e,null,ue),$e())},i(t){if(!Y){tt(A.$$.fragment,t),tt(H.$$.fragment,t);for(let a=0;a<z.length;a+=1)tt(v[a]);Y=!0}},o(t){et(A.$$.fragment,t),et(H.$$.fragment,t);for(let a=0;a<v.length;a+=1)et(v[a]);Y=!1},d(t){t&&c(e),t&&c(p),t&&c(d),$&&$.d(),t&&c(ot),Mt(A,t),t&&c(at),t&&c(F),t&&c(st),t&&c(M),t&&c(rt),t&&c(I),t&&c(ct),t&&c(U),w&&w.d(),y&&y.d(),g&&g.d(),t&&c(_t),t&&c(j),t&&c(kt),t&&c(E),Mt(H),t&&c(Rt),t&&c(J),t&&c(Ct),t&&c(L);for(let a=0;a<T.length;a+=1)T[a].d();for(let a=0;a<v.length;a+=1)v[a].d()}}}function Ee(n,e,l){let i,S,m,p,{collection:d=new Re}=e,_=200,R=[];const C=O=>l(3,_=O.code);return n.$$set=O=>{"collection"in O&&l(0,d=O.collection)},n.$$.update=()=>{var O,B;n.$$.dirty&1&&l(2,S=(O=d==null?void 0:d.options)==null?void 0:O.allowEmailAuth),n.$$.dirty&1&&l(1,m=(B=d==null?void 0:d.options)==null?void 0:B.allowUsernameAuth),n.$$.dirty&6&&l(5,p=m&&S?"YOUR_USERNAME_OR_EMAIL":m?"YOUR_USERNAME":"YOUR_EMAIL"),n.$$.dirty&1&&l(4,R=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:de.dummyCollectionRecord(d)},null,2)},{code:400,body:`
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to authenticate.",
|
||||
"data": {
|
||||
"identity": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`}])},l(6,i=de.getApiExampleUrl(Ce.baseUrl)),[d,m,S,_,R,p,i,C]}class Be extends Se{constructor(e){super(),ve(this,e,Ee,De,we,{collection:0})}}export{Be as default};
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import{S as Pt,i as mt,s as Xt,e as Zt,f as bt,U as B,g as wt,y as je,o as kt,I as yt,J as xt,K as Yt,H as vt,C as Tt,L as Ut}from"./index-6e04d404.js";import{P as qt,N as _t,u as Wt,D as Vt,v as Ye,T as M,I as ve,w as le,x as l,y as Ct,L as oe,z as Qe,A as I,B as ce,F as xO,G as ue,H as R,J as YO,K as vO,M as TO,E as T,O as E,Q as Rt,R as jt,U as UO,V as X,W as Gt,X as zt,a as j,h as At,b as It,c as Et,d as Nt,e as Dt,s as Bt,t as Jt,f as Lt,g as Mt,r as Ft,i as Ht,k as Kt,j as ea,l as Oa,m as ta,n as aa,o as ia,p as ra,q as Ge,C as J}from"./index-91aae1cf.js";class ee{constructor(e,a,t,i,s,r,n,o,c,u=0,Q){this.p=e,this.stack=a,this.state=t,this.reducePos=i,this.pos=s,this.score=r,this.buffer=n,this.bufferBase=o,this.curContext=c,this.lookAhead=u,this.parent=Q}toString(){return`[${this.stack.filter((e,a)=>a%3==0).concat(this.state)}]@${this.pos}${this.score?"!"+this.score:""}`}static start(e,a,t=0){let i=e.parser.context;return new ee(e,[],a,t,t,0,[],0,i?new ze(i,i.start):null,0,null)}get context(){return this.curContext?this.curContext.context:null}pushState(e,a){this.stack.push(this.state,a,this.bufferBase+this.buffer.length),this.state=e}reduce(e){var a;let t=e>>19,i=e&65535,{parser:s}=this.p,r=s.dynamicPrecedence(i);if(r&&(this.score+=r),t==0){this.pushState(s.getGoto(this.state,i,!0),this.reducePos),i<s.minRepeatTerm&&this.storeNode(i,this.reducePos,this.reducePos,4,!0),this.reduceContext(i,this.reducePos);return}let n=this.stack.length-(t-1)*3-(e&262144?6:0),o=n?this.stack[n-2]:this.p.ranges[0].from,c=this.reducePos-o;c>=2e3&&!(!((a=this.p.parser.nodeSet.types[i])===null||a===void 0)&&a.isAnonymous)&&(o==this.p.lastBigReductionStart?(this.p.bigReductionCount++,this.p.lastBigReductionSize=c):this.p.lastBigReductionSize<c&&(this.p.bigReductionCount=1,this.p.lastBigReductionStart=o,this.p.lastBigReductionSize=c));let u=n?this.stack[n-1]:0,Q=this.bufferBase+this.buffer.length-u;if(i<s.minRepeatTerm||e&131072){let h=s.stateFlag(this.state,1)?this.pos:this.reducePos;this.storeNode(i,o,h,Q+4,!0)}if(e&262144)this.state=this.stack[n];else{let h=this.stack[n-3];this.state=s.getGoto(h,i,!0)}for(;this.stack.length>n;)this.stack.pop();this.reduceContext(i,o)}storeNode(e,a,t,i=4,s=!1){if(e==0&&(!this.stack.length||this.stack[this.stack.length-1]<this.buffer.length+this.bufferBase)){let r=this,n=this.buffer.length;if(n==0&&r.parent&&(n=r.bufferBase-r.parent.bufferBase,r=r.parent),n>0&&r.buffer[n-4]==0&&r.buffer[n-1]>-1){if(a==t)return;if(r.buffer[n-2]>=a){r.buffer[n-2]=t;return}}}if(!s||this.pos==t)this.buffer.push(e,a,t,i);else{let r=this.buffer.length;if(r>0&&this.buffer[r-4]!=0)for(;r>0&&this.buffer[r-2]>t;)this.buffer[r]=this.buffer[r-4],this.buffer[r+1]=this.buffer[r-3],this.buffer[r+2]=this.buffer[r-2],this.buffer[r+3]=this.buffer[r-1],r-=4,i>4&&(i-=4);this.buffer[r]=e,this.buffer[r+1]=a,this.buffer[r+2]=t,this.buffer[r+3]=i}}shift(e,a,t){let i=this.pos;if(e&131072)this.pushState(e&65535,this.pos);else if(e&262144)this.pos=t,this.shiftContext(a,i),a<=this.p.parser.maxNode&&this.buffer.push(a,i,t,4);else{let s=e,{parser:r}=this.p;(t>this.pos||a<=r.maxNode)&&(this.pos=t,r.stateFlag(s,1)||(this.reducePos=t)),this.pushState(s,i),this.shiftContext(a,i),a<=r.maxNode&&this.buffer.push(a,i,t,4)}}apply(e,a,t){e&65536?this.reduce(e):this.shift(e,a,t)}useNode(e,a){let t=this.p.reused.length-1;(t<0||this.p.reused[t]!=e)&&(this.p.reused.push(e),t++);let i=this.pos;this.reducePos=this.pos=i+e.length,this.pushState(a,i),this.buffer.push(t,i,this.reducePos,-1),this.curContext&&this.updateContext(this.curContext.tracker.reuse(this.curContext.context,e,this,this.p.stream.reset(this.pos-e.length)))}split(){let e=this,a=e.buffer.length;for(;a>0&&e.buffer[a-2]>e.reducePos;)a-=4;let t=e.buffer.slice(a),i=e.bufferBase+a;for(;e&&i==e.bufferBase;)e=e.parent;return new ee(this.p,this.stack.slice(),this.state,this.reducePos,this.pos,this.score,t,i,this.curContext,this.lookAhead,e)}recoverByDelete(e,a){let t=e<=this.p.parser.maxNode;t&&this.storeNode(e,this.pos,a,4),this.storeNode(0,this.pos,a,t?8:4),this.pos=this.reducePos=a,this.score-=190}canShift(e){for(let a=new sa(this);;){let t=this.p.parser.stateSlot(a.state,4)||this.p.parser.hasAction(a.state,e);if(t==0)return!1;if(!(t&65536))return!0;a.reduce(t)}}recoverByInsert(e){if(this.stack.length>=300)return[];let a=this.p.parser.nextStates(this.state);if(a.length>8||this.stack.length>=120){let i=[];for(let s=0,r;s<a.length;s+=2)(r=a[s+1])!=this.state&&this.p.parser.hasAction(r,e)&&i.push(a[s],r);if(this.stack.length<120)for(let s=0;i.length<8&&s<a.length;s+=2){let r=a[s+1];i.some((n,o)=>o&1&&n==r)||i.push(a[s],r)}a=i}let t=[];for(let i=0;i<a.length&&t.length<4;i+=2){let s=a[i+1];if(s==this.state)continue;let r=this.split();r.pushState(s,this.pos),r.storeNode(0,r.pos,r.pos,4,!0),r.shiftContext(a[i],this.pos),r.score-=200,t.push(r)}return t}forceReduce(){let{parser:e}=this.p,a=e.stateSlot(this.state,5);if(!(a&65536))return!1;if(!e.validAction(this.state,a)){let t=a>>19,i=a&65535,s=this.stLine truncated
|
||||
\${}
|
||||
} catch (\${error}) {
|
||||
\${}
|
||||
}`,{label:"try",detail:"/ catch block",type:"keyword"}),X("if (${}) {\n ${}\n}",{label:"if",detail:"block",type:"keyword"}),X(`if (\${}) {
|
||||
\${}
|
||||
} else {
|
||||
\${}
|
||||
}`,{label:"if",detail:"/ else block",type:"keyword"}),X(`class \${name} {
|
||||
constructor(\${params}) {
|
||||
\${}
|
||||
}
|
||||
}`,{label:"class",detail:"definition",type:"keyword"}),X('import {${names}} from "${module}"\n${}',{label:"import",detail:"named",type:"keyword"}),X('import ${name} from "${module}"\n${}',{label:"import",detail:"default",type:"keyword"})],fO=new YO,MO=new Set(["Script","Block","FunctionExpression","FunctionDeclaration","ArrowFunction","MethodDeclaration","ForStatement"]);function G(O){return(e,a)=>{let t=e.node.getChild("VariableDefinition");return t&&a(t,O),!0}}const Ii=["FunctionDeclaration"],Ei={FunctionDeclaration:G("function"),ClassDeclaration:G("class"),ClassExpression:()=>!0,EnumDeclaration:G("constant"),TypeAliasDeclaration:G("type"),NamespaceDeclaration:G("namespace"),VariableDefinition(O,e){O.matchContext(Ii)||e(O,"variable")},TypeDefinition(O,e){e(O,"type")},__proto__:null};function FO(O,e){let a=fO.get(e);if(a)return a;let t=[],i=!0;function s(r,n){let o=O.sliceString(r.from,r.to);t.push({label:o,type:n})}return e.cursor(ve.IncludeAnonymous).iterate(r=>{if(i)i=!1;else if(r.name){let n=Ei[r.name];if(n&&n(r,s)||MO.has(r.name))return!1}else if(r.to-r.from>8192){for(let n of FO(O,r.node))t.push(n);return!1}}),fO.set(e,t),t}const hO=/^[\w$\xa1-\uffff][\w$\d\xa1-\uffff]*$/,HO=["TemplateString","String","RegExp","LineComment","BlockComment","VariableDefinition","TypeDefinition","Label","PropertyDefinition","PropertyName","PrivatePropertyDefinition","PrivatePropertyName",".","?."];function Ni(O){let e=R(O.state).resolveInner(O.pos,-1);if(HO.indexOf(e.name)>-1)return null;let a=e.name=="VariableName"||e.to-e.from<20&&hO.test(O.state.sliceDoc(e.from,e.to));if(!a&&!O.explicit)return null;let t=[];for(let i=e;i;i=i.parent)MO.has(i.name)&&(t=t.concat(FO(O.state.doc,i)));return{options:t,from:a?e.from:O.pos,validFor:hO}}const y=oe.define({name:"javascript",parser:zi.configure({props:[Qe.add({IfStatement:I({except:/^\s*({|else\b)/}),TryStatement:I({except:/^\s*({|catch\b|finally\b)/}),LabeledStatement:Rt,SwitchBody:O=>{let e=O.textAfter,a=/^\s*\}/.test(e),t=/^\s*(case|default)\b/.test(e);return O.baseIndent+(a?0:t?1:2)*O.unit},Block:jt({closing:"}"}),ArrowFunction:O=>O.baseIndent+O.unit,"TemplateString BlockComment":()=>null,"Statement Property":I({except:/^{/}),JSXElement(O){let e=/^\s*<\//.test(O.textAfter);return O.lineIndent(O.node.from)+(e?0:O.unit)},JSXEscape(O){let e=/\s*\}/.test(O.textAfter);return O.lineIndent(O.node.from)+(e?0:O.unit)},"JSXOpenTag JSXSelfClosingTag"(O){return O.column(O.node.from)+O.unit}}),ce.add({"Block ClassBody SwitchBody EnumBody ObjectExpression ArrayExpression ObjectType":xO,BlockComment(O){return{from:O.from+2,to:O.to-2}}})]}),languageData:{closeBrackets:{brackets:["(","[","{","'",'"',"`"]},commentTokens:{line:"//",block:{open:"/*",close:"*/"}},indentOnInput:/^\s*(?:case |default:|\{|\}|<\/)$/,wordChars:"$"}}),KO={test:O=>/^JSX/.test(O.name),facet:Gt({commentTokens:{block:{open:"{/*",close:"*/}"}}})},et=y.configure({dialect:"ts"},"typescript"),Ot=y.configure({dialect:"jsx",props:[UO.add(O=>O.isTop?[KO]:void 0)]}),tt=y.configure({dialect:"jsx ts",props:[UO.add(O=>O.isTop?[KO]:void 0)]},"typescript"),Di="break case const continue default delete export extends false finally in instanceof let new return static super switch this throw true typeof var yield".split(" ").map(O=>({label:O,type:"keyword"}));function at(O={}){let e=O.jsx?O.typescript?tt:Ot:O.typescript?et:y;return new ue(e,[y.data.of({autocomplete:vO(HO,TO(Ai.concat(Di)))}),y.data.of({autocomplete:Ni}),O.jsx?Mi:[]])}function Bi(O){for(;;){if(O.name=="JSXOpenTag"||O.name=="JSXSelfClosingTag"||O.name=="JSXFragmentTag")return O;if(O.name=="JSXEscape"||!O.parent)return null;O=O.parent}}function pO(O,e,a=O.length){for(let t=e==null?void 0:e.firstChild;t;t=t.nextSibling)if(t.name=="JSXIdentifier"||t.name=="JSXBuiltin"||t.name=="JSXNamespacedName"||t.name=="JSXMemberExpression")return O.sliceString(t.from,Math.min(t.to,a));return""}function Ji(O){return O&&(O.name=="JSXEndTag"||O.name=="JSXSelfCloseEndTag")}const Li=typeof navigator=="object"&&/Android\b/.test(navigator.userAgent),Mi=T.inputHandler.of((O,e,a,t)=>{if((Li?O.composing:O.compositionStarted)||O.state.readOnly||e!=a||t!=">"&&t!="/"||!y.isActiveAt(O.state,e,-1))return!1;let{state:i}=O,s=i.changeByRange(r=>{var n;let{head:o}=r,c=R(i).resolveInner(o,-1),u;if(c.name=="JSXStartTag"&&(c=c.parent),!(c.name=="JSXAttributeValue"&&c.to>o)){if(t==">"&&c.name=="JSXFragmentTag")return{range:E.cursor(o+1),changes:{from:o,insert:"></>"}};if(t=="/"&&c.name=="JSXFragmentTag"){let Q=c.parent,h=Q==null?void 0:Q.parent;if(Q.from==o-1&&((n=h.lastChild)===null||n===void 0?void 0:n.name)!="JSXEndTag"&&(u=pO(i.doc,h==null?void 0:h.firstChild,o))){let f=`/${u}>`;return{range:E.cursor(o+f.length),changes:{from:o,insert:f}}}}else if(t==">"){let Q=Bi(c);if(Q&&!Ji(Q.lastChild)&&i.sliceDoc(o,o+2)!="</"&&(u=pO(i.doc,Q,o)))return{range:E.cursor(o+1),changes:{from:o,insert:`></${u}>`}}}}return{range:r}});return s.changes.empty?!1:(O.dispatch(s,{userEvent:"input.type",scrollIntoView:!0}),!0)}),z=["
Line truncated
|
||||
`;function ht(O,e,a){let t=Object.create(null);t.true=t.false=nr,t.null=t.unknown=lr;for(let i of O.split(" "))i&&(t[i]=ct);for(let i of e.split(" "))i&&(t[i]=ut);for(let i of(a||"").split(" "))i&&(t[i]=Xr);return t}const kr="array binary bit boolean char character clob date decimal double float int integer interval large national nchar nclob numeric object precision real smallint time timestamp varchar varying ",yr="absolute action add after all allocate alter and any are as asc assertion at authorization before begin between both breadth by call cascade cascaded case cast catalog check close collate collation column commit condition connect connection constraint constraints constructor continue corresponding count create cross cube current current_date current_default_transform_group current_transform_group_for_type current_path current_role current_time current_timestamp current_user cursor cycle data day deallocate declare default deferrable deferred delete depth deref desc describe descriptor deterministic diagnostics disconnect distinct do domain drop dynamic each else elseif end end-exec equals escape except exception exec execute exists exit external fetch first for foreign found from free full function general get global go goto grant group grouping handle having hold hour identity if immediate in indicator initially inner inout input insert intersect into is isolation join key language last lateral leading leave left level like limit local localtime localtimestamp locator loop map match method minute modifies module month names natural nesting new next no none not of old on only open option or order ordinality out outer output overlaps pad parameter partial path prepare preserve primary prior privileges procedure public read reads recursive redo ref references referencing relative release repeat resignal restrict result return returns revoke right role rollback rollup routine row rows savepoint schema scroll search second section select session session_user set sets signal similar size some space specific specifictype sql sqlexception sqlstate sqlwarning start state static system_user table temporary then timezone_hour timezone_minute to trailing transaction translation treat trigger under undo union unique unnest until update usage user using value values view when whenever where while with without work write year zone ",ye={backslashEscapes:!1,hashComments:!1,spaceAfterDashes:!1,slashComments:!1,doubleQuotedStrings:!1,doubleDollarQuotedStrings:!1,unquotedBitLiterals:!1,treatBitsAsBytes:!1,charSetCasts:!1,operatorChars:"*+-%<>!=&|~^/",specialVar:"?",identifierQuotes:'"',words:ht(yr,kr)};function xr(O,e,a,t){let i={};for(let s in ye)i[s]=(O.hasOwnProperty(s)?O:ye)[s];return e&&(i.words=ht(e,a||"",t)),i}function pt(O){return new b(e=>{var a;let{next:t}=e;if(e.advance(),_(t,wO)){for(;_(e.next,wO);)e.advance();e.acceptToken(rr)}else if(t==36&&e.next==36&&O.doubleDollarQuotedStrings)br(e),e.acceptToken(L);else if(t==39||t==34&&O.doubleQuotedStrings)v(e,t,O.backslashEscapes),e.acceptToken(L);else if(t==35&&O.hashComments||t==47&&e.next==47&&O.slashComments)bO(e),e.acceptToken(gO);else if(t==45&&e.next==45&&(!O.spaceAfterDashes||e.peek(1)==32))bO(e),e.acceptToken(gO);else if(t==47&&e.next==42){e.advance();for(let i=1;;){let s=e.next;if(e.next<0)break;if(e.advance(),s==42&&e.next==47){if(i--,e.advance(),!i)break}else s==47&&e.next==42&&(i++,e.advance())}e.acceptToken(sr)}else if((t==101||t==69)&&e.next==39)e.advance(),v(e,39,!0);else if((t==110||t==78)&&e.next==39&&O.charSetCasts)e.advance(),v(e,39,O.backslashEscapes),e.acceptToken(L);else if(t==95&&O.charSetCasts)for(let i=0;;i++){if(e.next==39&&i>1){e.advance(),v(e,39,O.backslashEscapes),e.acceptToken(L);break}if(!ke(e.next))break;e.advance()}else if(t==40)e.acceptToken(or);else if(t==41)e.acceptToken(Qr);else if(t==123)e.acceptToken(cr);else if(t==125)e.acceptToken(ur);else if(t==91)e.acceptToken(fr);else if(t==93)e.acceptToken(hr);else if(t==59)e.acceptToken(pr);else if(O.unquotedBitLiterals&&t==48&&e.next==98)e.advance(),XO(e),e.acceptToken(mO);else if((t==98||t==66)&&(e.next==39||e.next==34)){const i=e.next;e.advance(),O.treatBitsAsBytes?(v(e,i,O.backslashEscapes),e.acceptToken(mr)):(XO(e,i),e.acceptToken(mO))}else if(t==48&&(e.next==120||e.next==88)||(t==120||t==88)&&e.next==39){let i=e.next==39;for(e.advance();Zr(e.next);)e.advance();i&&e.next==39&&e.advance(),e.acceptToken(Xe)}else if(t==46&&e.next>=48&&e.next<=57)ZO(e,!0),e.acceptToken(Xe);else if(t==46)e.acceptToken(dr);else if(t>=48&&t<=57)ZO(e,!1),e.acceptToken(Xe);else if(_(t,O.operatorChars)){for(;_(e.next,O.operatorChars);)e.advance();e.acceptToken(Sr)}else if(_(t,O.specialVar))e.next==t&&e.advance(),wr(e),e.acceptToken(gr);else if(_(t,O.identifierQuotes))v(e,t,!1),e.acceptToken(Pr);else if(t==58||t==44)e.acceptToken($r);else if(ke(t)){let i=ft(e,String.fromCharCode(t));e.acceptToken(e.next==46?PO:(a=O.words[i.toLowerCase()])!==null&&a!==void 0?a:PO)}})}const dt=pt(ye),Yr=U.deserialize({
Line truncated
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import{S as gt,i as Pt,s as mt,e as Xt,f as Zt,U as N,g as bt,y as RO,o as kt,I as xt,J as yt,K as wt,H as Yt,C as vt,L as Tt}from"./index-79bb649c.js";import{P as Ut,N as Wt,u as _t,D as Ct,v as wO,T as D,I as YO,w as rO,x as l,y as Vt,L as sO,z as nO,A as j,B as lO,F as ye,G as oO,H as C,J as we,K as Ye,M as ve,E as v,O as G,Q as qt,R as Rt,U as Te,V as X,W as zt,X as jt,a as V,h as Gt,b as At,c as It,d as Nt,e as Et,s as Bt,t as Dt,f as Jt,g as Lt,r as Mt,i as Ht,k as Ft,j as Kt,l as Oa,m as ea,n as ta,o as aa,p as ia,q as zO,C as E}from"./index-8390b00e.js";class H{constructor(O,a,t,i,r,s,n,o,c,u=0,Q){this.p=O,this.stack=a,this.state=t,this.reducePos=i,this.pos=r,this.score=s,this.buffer=n,this.bufferBase=o,this.curContext=c,this.lookAhead=u,this.parent=Q}toString(){return`[${this.stack.filter((O,a)=>a%3==0).concat(this.state)}]@${this.pos}${this.score?"!"+this.score:""}`}static start(O,a,t=0){let i=O.parser.context;return new H(O,[],a,t,t,0,[],0,i?new jO(i,i.start):null,0,null)}get context(){return this.curContext?this.curContext.context:null}pushState(O,a){this.stack.push(this.state,a,this.bufferBase+this.buffer.length),this.state=O}reduce(O){var a;let t=O>>19,i=O&65535,{parser:r}=this.p,s=r.dynamicPrecedence(i);if(s&&(this.score+=s),t==0){this.pushState(r.getGoto(this.state,i,!0),this.reducePos),i<r.minRepeatTerm&&this.storeNode(i,this.reducePos,this.reducePos,4,!0),this.reduceContext(i,this.reducePos);return}let n=this.stack.length-(t-1)*3-(O&262144?6:0),o=n?this.stack[n-2]:this.p.ranges[0].from,c=this.reducePos-o;c>=2e3&&!(!((a=this.p.parser.nodeSet.types[i])===null||a===void 0)&&a.isAnonymous)&&(o==this.p.lastBigReductionStart?(this.p.bigReductionCount++,this.p.lastBigReductionSize=c):this.p.lastBigReductionSize<c&&(this.p.bigReductionCount=1,this.p.lastBigReductionStart=o,this.p.lastBigReductionSize=c));let u=n?this.stack[n-1]:0,Q=this.bufferBase+this.buffer.length-u;if(i<r.minRepeatTerm||O&131072){let f=r.stateFlag(this.state,1)?this.pos:this.reducePos;this.storeNode(i,o,f,Q+4,!0)}if(O&262144)this.state=this.stack[n];else{let f=this.stack[n-3];this.state=r.getGoto(f,i,!0)}for(;this.stack.length>n;)this.stack.pop();this.reduceContext(i,o)}storeNode(O,a,t,i=4,r=!1){if(O==0&&(!this.stack.length||this.stack[this.stack.length-1]<this.buffer.length+this.bufferBase)){let s=this,n=this.buffer.length;if(n==0&&s.parent&&(n=s.bufferBase-s.parent.bufferBase,s=s.parent),n>0&&s.buffer[n-4]==0&&s.buffer[n-1]>-1){if(a==t)return;if(s.buffer[n-2]>=a){s.buffer[n-2]=t;return}}}if(!r||this.pos==t)this.buffer.push(O,a,t,i);else{let s=this.buffer.length;if(s>0&&this.buffer[s-4]!=0)for(;s>0&&this.buffer[s-2]>t;)this.buffer[s]=this.buffer[s-4],this.buffer[s+1]=this.buffer[s-3],this.buffer[s+2]=this.buffer[s-2],this.buffer[s+3]=this.buffer[s-1],s-=4,i>4&&(i-=4);this.buffer[s]=O,this.buffer[s+1]=a,this.buffer[s+2]=t,this.buffer[s+3]=i}}shift(O,a,t){let i=this.pos;if(O&131072)this.pushState(O&65535,this.pos);else if(O&262144)this.pos=t,this.shiftContext(a,i),a<=this.p.parser.maxNode&&this.buffer.push(a,i,t,4);else{let r=O,{parser:s}=this.p;(t>this.pos||a<=s.maxNode)&&(this.pos=t,s.stateFlag(r,1)||(this.reducePos=t)),this.pushState(r,i),this.shiftContext(a,i),a<=s.maxNode&&this.buffer.push(a,i,t,4)}}apply(O,a,t){O&65536?this.reduce(O):this.shift(O,a,t)}useNode(O,a){let t=this.p.reused.length-1;(t<0||this.p.reused[t]!=O)&&(this.p.reused.push(O),t++);let i=this.pos;this.reducePos=this.pos=i+O.length,this.pushState(a,i),this.buffer.push(t,i,this.reducePos,-1),this.curContext&&this.updateContext(this.curContext.tracker.reuse(this.curContext.context,O,this,this.p.stream.reset(this.pos-O.length)))}split(){let O=this,a=O.buffer.length;for(;a>0&&O.buffer[a-2]>O.reducePos;)a-=4;let t=O.buffer.slice(a),i=O.bufferBase+a;for(;O&&i==O.bufferBase;)O=O.parent;return new H(this.p,this.stack.slice(),this.state,this.reducePos,this.pos,this.score,t,i,this.curContext,this.lookAhead,O)}recoverByDelete(O,a){let t=O<=this.p.parser.maxNode;t&&this.storeNode(O,this.pos,a,4),this.storeNode(0,this.pos,a,t?8:4),this.pos=this.reducePos=a,this.score-=190}canShift(O){for(let a=new ra(this);;){let t=this.p.parser.stateSlot(a.state,4)||this.p.parser.hasAction(a.state,O);if(t==0)return!1;if(!(t&65536))return!0;a.reduce(t)}}recoverByInsert(O){if(this.stack.length>=300)return[];let a=this.p.parser.nextStates(this.state);if(a.length>4<<1||this.stack.length>=120){let i=[];for(let r=0,s;r<a.length;r+=2)(s=a[r+1])!=this.state&&this.p.parser.hasAction(s,O)&&i.push(a[r],s);if(this.stack.length<120)for(let r=0;i.length<4<<1&&r<a.length;r+=2){let s=a[r+1];i.some((n,o)=>o&1&&n==s)||i.push(a[r],s)}a=i}let t=[];for(let i=0;i<a.length&&t.length<4;i+=2){let r=a[i+1];if(r==this.state)continue;let s=this.split();s.pushState(r,this.pos),s.storeNode(0,s.pos,s.pos,4,!0),s.shiftContext(a[i],this.pos),s.score-=200,t.push(s)}return t}forceReduce(){let O=this.p.parser.stateSlot(this.state,5);if(!(O&65536))return!1;let{parser:a}=this.p;if(!a.validAction(this.state,O)){let t=O>>19,Line truncated
|
||||
\${}
|
||||
} catch (\${error}) {
|
||||
\${}
|
||||
}`,{label:"try",detail:"/ catch block",type:"keyword"}),X("if (${}) {\n ${}\n}",{label:"if",detail:"block",type:"keyword"}),X(`if (\${}) {
|
||||
\${}
|
||||
} else {
|
||||
\${}
|
||||
}`,{label:"if",detail:"/ else block",type:"keyword"}),X(`class \${name} {
|
||||
constructor(\${params}) {
|
||||
\${}
|
||||
}
|
||||
}`,{label:"class",detail:"definition",type:"keyword"}),X('import {${names}} from "${module}"\n${}',{label:"import",detail:"named",type:"keyword"}),X('import ${name} from "${module}"\n${}',{label:"import",detail:"default",type:"keyword"})],he=new we,Le=new Set(["Script","Block","FunctionExpression","FunctionDeclaration","ArrowFunction","MethodDeclaration","ForStatement"]);function q(e){return(O,a)=>{let t=O.node.getChild("VariableDefinition");return t&&a(t,e),!0}}const Gi=["FunctionDeclaration"],Ai={FunctionDeclaration:q("function"),ClassDeclaration:q("class"),ClassExpression:()=>!0,EnumDeclaration:q("constant"),TypeAliasDeclaration:q("type"),NamespaceDeclaration:q("namespace"),VariableDefinition(e,O){e.matchContext(Gi)||O(e,"variable")},TypeDefinition(e,O){O(e,"type")},__proto__:null};function Me(e,O){let a=he.get(O);if(a)return a;let t=[],i=!0;function r(s,n){let o=e.sliceString(s.from,s.to);t.push({label:o,type:n})}return O.cursor(YO.IncludeAnonymous).iterate(s=>{if(i)i=!1;else if(s.name){let n=Ai[s.name];if(n&&n(s,r)||Le.has(s.name))return!1}else if(s.to-s.from>8192){for(let n of Me(e,s.node))t.push(n);return!1}}),he.set(O,t),t}const ue=/^[\w$\xa1-\uffff][\w$\d\xa1-\uffff]*$/,He=["TemplateString","String","RegExp","LineComment","BlockComment","VariableDefinition","TypeDefinition","Label","PropertyDefinition","PropertyName","PrivatePropertyDefinition","PrivatePropertyName"];function Ii(e){let O=C(e.state).resolveInner(e.pos,-1);if(He.indexOf(O.name)>-1)return null;let a=O.name=="VariableName"||O.to-O.from<20&&ue.test(e.state.sliceDoc(O.from,O.to));if(!a&&!e.explicit)return null;let t=[];for(let i=O;i;i=i.parent)Le.has(i.name)&&(t=t.concat(Me(e.state.doc,i)));return{options:t,from:a?O.from:e.pos,validFor:ue}}const x=sO.define({name:"javascript",parser:zi.configure({props:[nO.add({IfStatement:j({except:/^\s*({|else\b)/}),TryStatement:j({except:/^\s*({|catch\b|finally\b)/}),LabeledStatement:qt,SwitchBody:e=>{let O=e.textAfter,a=/^\s*\}/.test(O),t=/^\s*(case|default)\b/.test(O);return e.baseIndent+(a?0:t?1:2)*e.unit},Block:Rt({closing:"}"}),ArrowFunction:e=>e.baseIndent+e.unit,"TemplateString BlockComment":()=>null,"Statement Property":j({except:/^{/}),JSXElement(e){let O=/^\s*<\//.test(e.textAfter);return e.lineIndent(e.node.from)+(O?0:e.unit)},JSXEscape(e){let O=/\s*\}/.test(e.textAfter);return e.lineIndent(e.node.from)+(O?0:e.unit)},"JSXOpenTag JSXSelfClosingTag"(e){return e.column(e.node.from)+e.unit}}),lO.add({"Block ClassBody SwitchBody EnumBody ObjectExpression ArrayExpression ObjectType":ye,BlockComment(e){return{from:e.from+2,to:e.to-2}}})]}),languageData:{closeBrackets:{brackets:["(","[","{","'",'"',"`"]},commentTokens:{line:"//",block:{open:"/*",close:"*/"}},indentOnInput:/^\s*(?:case |default:|\{|\}|<\/)$/,wordChars:"$"}}),Fe={test:e=>/^JSX/.test(e.name),facet:zt({commentTokens:{block:{open:"{/*",close:"*/}"}}})},Ke=x.configure({dialect:"ts"},"typescript"),Ot=x.configure({dialect:"jsx",props:[Te.add(e=>e.isTop?[Fe]:void 0)]}),et=x.configure({dialect:"jsx ts",props:[Te.add(e=>e.isTop?[Fe]:void 0)]},"typescript"),Ni="break case const continue default delete export extends false finally in instanceof let new return static super switch this throw true typeof var yield".split(" ").map(e=>({label:e,type:"keyword"}));function tt(e={}){let O=e.jsx?e.typescript?et:Ot:e.typescript?Ke:x;return new oO(O,[x.data.of({autocomplete:Ye(He,ve(ji.concat(Ni)))}),x.data.of({autocomplete:Ii}),e.jsx?Ji:[]])}function Ei(e){for(;;){if(e.name=="JSXOpenTag"||e.name=="JSXSelfClosingTag"||e.name=="JSXFragmentTag")return e;if(e.name=="JSXEscape"||!e.parent)return null;e=e.parent}}function fe(e,O,a=e.length){for(let t=O==null?void 0:O.firstChild;t;t=t.nextSibling)if(t.name=="JSXIdentifier"||t.name=="JSXBuiltin"||t.name=="JSXNamespacedName"||t.name=="JSXMemberExpression")return e.sliceString(t.from,Math.min(t.to,a));return""}function Bi(e){return e&&(e.name=="JSXEndTag"||e.name=="JSXSelfCloseEndTag")}const Di=typeof navigator=="object"&&/Android\b/.test(navigator.userAgent),Ji=v.inputHandler.of((e,O,a,t)=>{if((Di?e.composing:e.compositionStarted)||e.state.readOnly||O!=a||t!=">"&&t!="/"||!x.isActiveAt(e.state,O,-1))return!1;let{state:i}=e,r=i.changeByRange(s=>{var n;let{head:o}=s,c=C(i).resolveInner(o,-1),u;if(c.name=="JSXStartTag"&&(c=c.parent),!(c.name=="JSXAttributeValue"&&c.to>o)){if(t==">"&&c.name=="JSXFragmentTag")return{range:G.cursor(o+1),changes:{from:o,insert:"></>"}};if(t=="/"&&c.name=="JSXFragmentTag"){let Q=c.parent,f=Q==null?void 0:Q.parent;if(Q.from==o-1&&((n=f.lastChild)===null||n===void 0?void 0:n.name)!="JSXEndTag"&&(u=fe(i.doc,f==null?void 0:f.firstChild,o))){let h=`/${u}>`;return{range:G.cursor(o+h.length),changes:{from:o,insert:h}}}}else if(t==">"){let Q=Ei(c);if(Q&&!Bi(Q.lastChild)&&i.sliceDoc(o,o+2)!="</"&&(u=fe(i.doc,Q,o)))return{range:G.cursor(o+1),changes:{from:o,insert:`></${u}>`}}}}return{range:s}});return r.changes.empty?!1:(e.dispatch(r,{userEvent:"input.type",scrollIntoView:!0}),!0)}),R=["_blank","
Line truncated
|
||||
`;function ut(e,O,a){let t=Object.create(null);t.true=t.false=rr,t.null=t.unknown=sr;for(let i of e.split(" "))i&&(t[i]=Qt);for(let i of O.split(" "))i&&(t[i]=ct);for(let i of(a||"").split(" "))i&&(t[i]=Pr);return t}const br="array binary bit boolean char character clob date decimal double float int integer interval large national nchar nclob numeric object precision real smallint time timestamp varchar varying ",kr="absolute action add after all allocate alter and any are as asc assertion at authorization before begin between both breadth by call cascade cascaded case cast catalog check close collate collation column commit condition connect connection constraint constraints constructor continue corresponding count create cross cube current current_date current_default_transform_group current_transform_group_for_type current_path current_role current_time current_timestamp current_user cursor cycle data day deallocate declare default deferrable deferred delete depth deref desc describe descriptor deterministic diagnostics disconnect distinct do domain drop dynamic each else elseif end end-exec equals escape except exception exec execute exists exit external fetch first for foreign found from free full function general get global go goto grant group grouping handle having hold hour identity if immediate in indicator initially inner inout input insert intersect into is isolation join key language last lateral leading leave left level like limit local localtime localtimestamp locator loop map match method minute modifies module month names natural nesting new next no none not of old on only open option or order ordinality out outer output overlaps pad parameter partial path prepare preserve primary prior privileges procedure public read reads recursive redo ref references referencing relative release repeat resignal restrict result return returns revoke right role rollback rollup routine row rows savepoint schema scroll search second section select session session_user set sets signal similar size some space specific specifictype sql sqlexception sqlstate sqlwarning start state static system_user table temporary then timezone_hour timezone_minute to trailing transaction translation treat trigger under undo union unique unnest until update usage user using value values view when whenever where while with without work write year zone ",xO={backslashEscapes:!1,hashComments:!1,spaceAfterDashes:!1,slashComments:!1,doubleQuotedStrings:!1,doubleDollarQuotedStrings:!1,unquotedBitLiterals:!1,treatBitsAsBytes:!1,charSetCasts:!1,operatorChars:"*+-%<>!=&|~^/",specialVar:"?",identifierQuotes:'"',words:ut(kr,br)};function xr(e,O,a,t){let i={};for(let r in xO)i[r]=(e.hasOwnProperty(r)?e:xO)[r];return O&&(i.words=ut(O,a||"",t)),i}function ft(e){return new b(O=>{var a;let{next:t}=O;if(O.advance(),U(t,be)){for(;U(O.next,be);)O.advance();O.acceptToken(ar)}else if(t==36&&O.next==36&&e.doubleDollarQuotedStrings)Xr(O),O.acceptToken(B);else if(t==39||t==34&&e.doubleQuotedStrings)Y(O,t,e.backslashEscapes),O.acceptToken(B);else if(t==35&&e.hashComments||t==47&&O.next==47&&e.slashComments)Ze(O),O.acceptToken($e);else if(t==45&&O.next==45&&(!e.spaceAfterDashes||O.peek(1)==32))Ze(O),O.acceptToken($e);else if(t==47&&O.next==42){O.advance();for(let i=1;;){let r=O.next;if(O.next<0)break;if(O.advance(),r==42&&O.next==47){if(i--,O.advance(),!i)break}else r==47&&O.next==42&&(i++,O.advance())}O.acceptToken(ir)}else if((t==101||t==69)&&O.next==39)O.advance(),Y(O,39,!0);else if((t==110||t==78)&&O.next==39&&e.charSetCasts)O.advance(),Y(O,39,e.backslashEscapes),O.acceptToken(B);else if(t==95&&e.charSetCasts)for(let i=0;;i++){if(O.next==39&&i>1){O.advance(),Y(O,39,e.backslashEscapes),O.acceptToken(B);break}if(!kO(O.next))break;O.advance()}else if(t==40)O.acceptToken(nr);else if(t==41)O.acceptToken(lr);else if(t==123)O.acceptToken(or);else if(t==125)O.acceptToken(Qr);else if(t==91)O.acceptToken(cr);else if(t==93)O.acceptToken(hr);else if(t==59)O.acceptToken(ur);else if(e.unquotedBitLiterals&&t==48&&O.next==98)O.advance(),me(O),O.acceptToken(Pe);else if((t==98||t==66)&&(O.next==39||O.next==34)){const i=O.next;O.advance(),e.treatBitsAsBytes?(Y(O,i,e.backslashEscapes),O.acceptToken(gr)):(me(O,i),O.acceptToken(Pe))}else if(t==48&&(O.next==120||O.next==88)||(t==120||t==88)&&O.next==39){let i=O.next==39;for(O.advance();mr(O.next);)O.advance();i&&O.next==39&&O.advance(),O.acceptToken(mO)}else if(t==46&&O.next>=48&&O.next<=57)Xe(O,!0),O.acceptToken(mO);else if(t==46)O.acceptToken(fr);else if(t>=48&&t<=57)Xe(O,!1),O.acceptToken(mO);else if(U(t,e.operatorChars)){for(;U(O.next,e.operatorChars);)O.advance();O.acceptToken(dr)}else if(U(t,e.specialVar))O.next==t&&O.advance(),Zr(O),O.acceptToken(Sr);else if(U(t,e.identifierQuotes))Y(O,t,!1),O.acceptToken($r);else if(t==58||t==44)O.acceptToken(pr);else if(kO(t)){let i=ht(O,String.fromCharCode(t));O.acceptToken(O.next==46?ge:(a=e.words[i.toLowerCase()])!==null&&a!==void 0?a:ge)}})}const dt=ft(xO),yr=T.deserialize({
Line truncated
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import{S as Ce,i as $e,s as we,e as r,w as g,b as h,c as he,f as b,g as f,h as n,m as ve,x as Y,N as pe,O as Pe,k as Se,P as Oe,n as Te,t as Z,a as x,o as m,d as ge,T as Re,C as Ee,p as ye,r as j,u as Be,M as qe}from"./index-79bb649c.js";import{S as Ae}from"./SdkTabs-22bd33d6.js";function ue(o,l,s){const a=o.slice();return a[5]=l[s],a}function be(o,l,s){const a=o.slice();return a[5]=l[s],a}function _e(o,l){let s,a=l[5].code+"",_,u,i,d;function p(){return l[4](l[5])}return{key:o,first:null,c(){s=r("button"),_=g(a),u=h(),b(s,"class","tab-item"),j(s,"active",l[1]===l[5].code),this.first=s},m(C,$){f(C,s,$),n(s,_),n(s,u),i||(d=Be(s,"click",p),i=!0)},p(C,$){l=C,$&4&&a!==(a=l[5].code+"")&&Y(_,a),$&6&&j(s,"active",l[1]===l[5].code)},d(C){C&&m(s),i=!1,d()}}}function ke(o,l){let s,a,_,u;return a=new qe({props:{content:l[5].body}}),{key:o,first:null,c(){s=r("div"),he(a.$$.fragment),_=h(),b(s,"class","tab-item"),j(s,"active",l[1]===l[5].code),this.first=s},m(i,d){f(i,s,d),ve(a,s,null),n(s,_),u=!0},p(i,d){l=i;const p={};d&4&&(p.content=l[5].body),a.$set(p),(!u||d&6)&&j(s,"active",l[1]===l[5].code)},i(i){u||(Z(a.$$.fragment,i),u=!0)},o(i){x(a.$$.fragment,i),u=!1},d(i){i&&m(s),ge(a)}}}function Ue(o){var re,fe;let l,s,a=o[0].name+"",_,u,i,d,p,C,$,D=o[0].name+"",H,ee,F,w,I,T,L,P,M,te,N,R,le,z,K=o[0].name+"",G,se,J,E,Q,y,V,B,X,S,q,v=[],ae=new Map,oe,A,k=[],ne=new Map,O;w=new Ae({props:{js:`
|
||||
import{S as Ce,i as $e,s as we,e as r,w as g,b as h,c as he,f as b,g as f,h as n,m as ve,x as Y,N as pe,P as Pe,k as Se,Q as Oe,n as Te,t as Z,a as x,o as m,d as ge,T as Re,C as Ee,p as ye,r as j,u as Be,M as qe}from"./index-6e04d404.js";import{S as Ae}from"./SdkTabs-1a82fff4.js";function ue(o,l,s){const a=o.slice();return a[5]=l[s],a}function be(o,l,s){const a=o.slice();return a[5]=l[s],a}function _e(o,l){let s,a=l[5].code+"",_,u,i,d;function p(){return l[4](l[5])}return{key:o,first:null,c(){s=r("button"),_=g(a),u=h(),b(s,"class","tab-item"),j(s,"active",l[1]===l[5].code),this.first=s},m(C,$){f(C,s,$),n(s,_),n(s,u),i||(d=Be(s,"click",p),i=!0)},p(C,$){l=C,$&4&&a!==(a=l[5].code+"")&&Y(_,a),$&6&&j(s,"active",l[1]===l[5].code)},d(C){C&&m(s),i=!1,d()}}}function ke(o,l){let s,a,_,u;return a=new qe({props:{content:l[5].body}}),{key:o,first:null,c(){s=r("div"),he(a.$$.fragment),_=h(),b(s,"class","tab-item"),j(s,"active",l[1]===l[5].code),this.first=s},m(i,d){f(i,s,d),ve(a,s,null),n(s,_),u=!0},p(i,d){l=i;const p={};d&4&&(p.content=l[5].body),a.$set(p),(!u||d&6)&&j(s,"active",l[1]===l[5].code)},i(i){u||(Z(a.$$.fragment,i),u=!0)},o(i){x(a.$$.fragment,i),u=!1},d(i){i&&m(s),ge(a)}}}function Ue(o){var re,fe;let l,s,a=o[0].name+"",_,u,i,d,p,C,$,D=o[0].name+"",H,ee,F,w,I,T,L,P,M,te,N,R,le,Q,K=o[0].name+"",z,se,G,E,J,y,V,B,X,S,q,v=[],ae=new Map,oe,A,k=[],ne=new Map,O;w=new Ae({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${o[3]}');
|
||||
@@ -20,7 +20,7 @@ import{S as Ce,i as $e,s as we,e as r,w as g,b as h,c as he,f as b,g as f,h as n
|
||||
'TOKEN',
|
||||
'YOUR_PASSWORD',
|
||||
);
|
||||
`}});let W=o[2];const ie=e=>e[5].code;for(let e=0;e<W.length;e+=1){let t=be(o,W,e),c=ie(t);ae.set(c,v[e]=_e(c,t))}let U=o[2];const ce=e=>e[5].code;for(let e=0;e<U.length;e+=1){let t=ue(o,U,e),c=ce(t);ne.set(c,k[e]=ke(c,t))}return{c(){l=r("h3"),s=g("Confirm email change ("),_=g(a),u=g(")"),i=h(),d=r("div"),p=r("p"),C=g("Confirms "),$=r("strong"),H=g(D),ee=g(" email change request."),F=h(),he(w.$$.fragment),I=h(),T=r("h6"),T.textContent="API details",L=h(),P=r("div"),M=r("strong"),M.textContent="POST",te=h(),N=r("div"),R=r("p"),le=g("/api/collections/"),z=r("strong"),G=g(K),se=g("/confirm-email-change"),J=h(),E=r("div"),E.textContent="Body Parameters",Q=h(),y=r("table"),y.innerHTML=`<thead><tr><th>Param</th>
|
||||
`}});let W=o[2];const ie=e=>e[5].code;for(let e=0;e<W.length;e+=1){let t=be(o,W,e),c=ie(t);ae.set(c,v[e]=_e(c,t))}let U=o[2];const ce=e=>e[5].code;for(let e=0;e<U.length;e+=1){let t=ue(o,U,e),c=ce(t);ne.set(c,k[e]=ke(c,t))}return{c(){l=r("h3"),s=g("Confirm email change ("),_=g(a),u=g(")"),i=h(),d=r("div"),p=r("p"),C=g("Confirms "),$=r("strong"),H=g(D),ee=g(" email change request."),F=h(),he(w.$$.fragment),I=h(),T=r("h6"),T.textContent="API details",L=h(),P=r("div"),M=r("strong"),M.textContent="POST",te=h(),N=r("div"),R=r("p"),le=g("/api/collections/"),Q=r("strong"),z=g(K),se=g("/confirm-email-change"),G=h(),E=r("div"),E.textContent="Body Parameters",J=h(),y=r("table"),y.innerHTML=`<thead><tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th></tr></thead>
|
||||
<tbody><tr><td><div class="inline-flex"><span class="label label-success">Required</span>
|
||||
@@ -30,7 +30,7 @@ import{S as Ce,i as $e,s as we,e as r,w as g,b as h,c as he,f as b,g as f,h as n
|
||||
<tr><td><div class="inline-flex"><span class="label label-success">Required</span>
|
||||
<span>password</span></div></td>
|
||||
<td><span class="label">String</span></td>
|
||||
<td>The account password to confirm the email change.</td></tr></tbody>`,V=h(),B=r("div"),B.textContent="Responses",X=h(),S=r("div"),q=r("div");for(let e=0;e<v.length;e+=1)v[e].c();oe=h(),A=r("div");for(let e=0;e<k.length;e+=1)k[e].c();b(l,"class","m-b-sm"),b(d,"class","content txt-lg m-b-sm"),b(T,"class","m-b-xs"),b(M,"class","label label-primary"),b(N,"class","content"),b(P,"class","alert alert-success"),b(E,"class","section-title"),b(y,"class","table-compact table-border m-b-base"),b(B,"class","section-title"),b(q,"class","tabs-header compact left"),b(A,"class","tabs-content"),b(S,"class","tabs")},m(e,t){f(e,l,t),n(l,s),n(l,_),n(l,u),f(e,i,t),f(e,d,t),n(d,p),n(p,C),n(p,$),n($,H),n(p,ee),f(e,F,t),ve(w,e,t),f(e,I,t),f(e,T,t),f(e,L,t),f(e,P,t),n(P,M),n(P,te),n(P,N),n(N,R),n(R,le),n(R,z),n(z,G),n(R,se),f(e,J,t),f(e,E,t),f(e,Q,t),f(e,y,t),f(e,V,t),f(e,B,t),f(e,X,t),f(e,S,t),n(S,q);for(let c=0;c<v.length;c+=1)v[c]&&v[c].m(q,null);n(S,oe),n(S,A);for(let c=0;c<k.length;c+=1)k[c]&&k[c].m(A,null);O=!0},p(e,[t]){var me,de;(!O||t&1)&&a!==(a=e[0].name+"")&&Y(_,a),(!O||t&1)&&D!==(D=e[0].name+"")&&Y(H,D);const c={};t&9&&(c.js=`
|
||||
<td>The account password to confirm the email change.</td></tr></tbody>`,V=h(),B=r("div"),B.textContent="Responses",X=h(),S=r("div"),q=r("div");for(let e=0;e<v.length;e+=1)v[e].c();oe=h(),A=r("div");for(let e=0;e<k.length;e+=1)k[e].c();b(l,"class","m-b-sm"),b(d,"class","content txt-lg m-b-sm"),b(T,"class","m-b-xs"),b(M,"class","label label-primary"),b(N,"class","content"),b(P,"class","alert alert-success"),b(E,"class","section-title"),b(y,"class","table-compact table-border m-b-base"),b(B,"class","section-title"),b(q,"class","tabs-header compact left"),b(A,"class","tabs-content"),b(S,"class","tabs")},m(e,t){f(e,l,t),n(l,s),n(l,_),n(l,u),f(e,i,t),f(e,d,t),n(d,p),n(p,C),n(p,$),n($,H),n(p,ee),f(e,F,t),ve(w,e,t),f(e,I,t),f(e,T,t),f(e,L,t),f(e,P,t),n(P,M),n(P,te),n(P,N),n(N,R),n(R,le),n(R,Q),n(Q,z),n(R,se),f(e,G,t),f(e,E,t),f(e,J,t),f(e,y,t),f(e,V,t),f(e,B,t),f(e,X,t),f(e,S,t),n(S,q);for(let c=0;c<v.length;c+=1)v[c]&&v[c].m(q,null);n(S,oe),n(S,A);for(let c=0;c<k.length;c+=1)k[c]&&k[c].m(A,null);O=!0},p(e,[t]){var me,de;(!O||t&1)&&a!==(a=e[0].name+"")&&Y(_,a),(!O||t&1)&&D!==(D=e[0].name+"")&&Y(H,D);const c={};t&9&&(c.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${e[3]}');
|
||||
@@ -52,7 +52,7 @@ import{S as Ce,i as $e,s as we,e as r,w as g,b as h,c as he,f as b,g as f,h as n
|
||||
'TOKEN',
|
||||
'YOUR_PASSWORD',
|
||||
);
|
||||
`),w.$set(c),(!O||t&1)&&K!==(K=e[0].name+"")&&Y(G,K),t&6&&(W=e[2],v=pe(v,t,ie,1,e,W,ae,q,Pe,_e,null,be)),t&6&&(U=e[2],Se(),k=pe(k,t,ce,1,e,U,ne,A,Oe,ke,null,ue),Te())},i(e){if(!O){Z(w.$$.fragment,e);for(let t=0;t<U.length;t+=1)Z(k[t]);O=!0}},o(e){x(w.$$.fragment,e);for(let t=0;t<k.length;t+=1)x(k[t]);O=!1},d(e){e&&m(l),e&&m(i),e&&m(d),e&&m(F),ge(w,e),e&&m(I),e&&m(T),e&&m(L),e&&m(P),e&&m(J),e&&m(E),e&&m(Q),e&&m(y),e&&m(V),e&&m(B),e&&m(X),e&&m(S);for(let t=0;t<v.length;t+=1)v[t].d();for(let t=0;t<k.length;t+=1)k[t].d()}}}function De(o,l,s){let a,{collection:_=new Re}=l,u=204,i=[];const d=p=>s(1,u=p.code);return o.$$set=p=>{"collection"in p&&s(0,_=p.collection)},s(3,a=Ee.getApiExampleUrl(ye.baseUrl)),s(2,i=[{code:204,body:"null"},{code:400,body:`
|
||||
`),w.$set(c),(!O||t&1)&&K!==(K=e[0].name+"")&&Y(z,K),t&6&&(W=e[2],v=pe(v,t,ie,1,e,W,ae,q,Pe,_e,null,be)),t&6&&(U=e[2],Se(),k=pe(k,t,ce,1,e,U,ne,A,Oe,ke,null,ue),Te())},i(e){if(!O){Z(w.$$.fragment,e);for(let t=0;t<U.length;t+=1)Z(k[t]);O=!0}},o(e){x(w.$$.fragment,e);for(let t=0;t<k.length;t+=1)x(k[t]);O=!1},d(e){e&&m(l),e&&m(i),e&&m(d),e&&m(F),ge(w,e),e&&m(I),e&&m(T),e&&m(L),e&&m(P),e&&m(G),e&&m(E),e&&m(J),e&&m(y),e&&m(V),e&&m(B),e&&m(X),e&&m(S);for(let t=0;t<v.length;t+=1)v[t].d();for(let t=0;t<k.length;t+=1)k[t].d()}}}function De(o,l,s){let a,{collection:_=new Re}=l,u=204,i=[];const d=p=>s(1,u=p.code);return o.$$set=p=>{"collection"in p&&s(0,_=p.collection)},s(3,a=Ee.getApiExampleUrl(ye.baseUrl)),s(2,i=[{code:204,body:"null"},{code:400,body:`
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to authenticate.",
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import{S as Se,i as he,s as Re,e as r,w as P,b as v,c as ve,f as b,g as d,h as n,m as we,x as K,N as me,O as Oe,k as Ne,P as Ce,n as We,t as Z,a as x,o as f,d as Pe,T as $e,C as Ee,p as Te,r as U,u as ge,M as Ae}from"./index-79bb649c.js";import{S as De}from"./SdkTabs-22bd33d6.js";function ue(o,s,l){const a=o.slice();return a[5]=s[l],a}function be(o,s,l){const a=o.slice();return a[5]=s[l],a}function _e(o,s){let l,a=s[5].code+"",_,u,i,p;function m(){return s[4](s[5])}return{key:o,first:null,c(){l=r("button"),_=P(a),u=v(),b(l,"class","tab-item"),U(l,"active",s[1]===s[5].code),this.first=l},m(S,h){d(S,l,h),n(l,_),n(l,u),i||(p=ge(l,"click",m),i=!0)},p(S,h){s=S,h&4&&a!==(a=s[5].code+"")&&K(_,a),h&6&&U(l,"active",s[1]===s[5].code)},d(S){S&&f(l),i=!1,p()}}}function ke(o,s){let l,a,_,u;return a=new Ae({props:{content:s[5].body}}),{key:o,first:null,c(){l=r("div"),ve(a.$$.fragment),_=v(),b(l,"class","tab-item"),U(l,"active",s[1]===s[5].code),this.first=l},m(i,p){d(i,l,p),we(a,l,null),n(l,_),u=!0},p(i,p){s=i;const m={};p&4&&(m.content=s[5].body),a.$set(m),(!u||p&6)&&U(l,"active",s[1]===s[5].code)},i(i){u||(Z(a.$$.fragment,i),u=!0)},o(i){x(a.$$.fragment,i),u=!1},d(i){i&&f(l),Pe(a)}}}function ye(o){var re,de;let s,l,a=o[0].name+"",_,u,i,p,m,S,h,M=o[0].name+"",j,ee,H,R,L,W,z,O,q,te,B,$,se,G,F=o[0].name+"",J,le,Q,E,V,T,X,g,Y,N,A,w=[],ae=new Map,oe,D,k=[],ne=new Map,C;R=new De({props:{js:`
|
||||
import{S as Se,i as he,s as Re,e as r,w as P,b as v,c as ve,f as b,g as d,h as n,m as we,x as K,N as me,P as Ne,k as Oe,Q as Ce,n as We,t as Z,a as x,o as f,d as Pe,T as $e,C as Ee,p as Te,r as U,u as ge,M as Ae}from"./index-6e04d404.js";import{S as De}from"./SdkTabs-1a82fff4.js";function ue(o,s,l){const a=o.slice();return a[5]=s[l],a}function be(o,s,l){const a=o.slice();return a[5]=s[l],a}function _e(o,s){let l,a=s[5].code+"",_,u,i,p;function m(){return s[4](s[5])}return{key:o,first:null,c(){l=r("button"),_=P(a),u=v(),b(l,"class","tab-item"),U(l,"active",s[1]===s[5].code),this.first=l},m(S,h){d(S,l,h),n(l,_),n(l,u),i||(p=ge(l,"click",m),i=!0)},p(S,h){s=S,h&4&&a!==(a=s[5].code+"")&&K(_,a),h&6&&U(l,"active",s[1]===s[5].code)},d(S){S&&f(l),i=!1,p()}}}function ke(o,s){let l,a,_,u;return a=new Ae({props:{content:s[5].body}}),{key:o,first:null,c(){l=r("div"),ve(a.$$.fragment),_=v(),b(l,"class","tab-item"),U(l,"active",s[1]===s[5].code),this.first=l},m(i,p){d(i,l,p),we(a,l,null),n(l,_),u=!0},p(i,p){s=i;const m={};p&4&&(m.content=s[5].body),a.$set(m),(!u||p&6)&&U(l,"active",s[1]===s[5].code)},i(i){u||(Z(a.$$.fragment,i),u=!0)},o(i){x(a.$$.fragment,i),u=!1},d(i){i&&f(l),Pe(a)}}}function ye(o){var re,de;let s,l,a=o[0].name+"",_,u,i,p,m,S,h,M=o[0].name+"",j,ee,H,R,L,W,Q,N,q,te,B,$,se,z,F=o[0].name+"",G,le,J,E,V,T,X,g,Y,O,A,w=[],ae=new Map,oe,D,k=[],ne=new Map,C;R=new De({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${o[3]}');
|
||||
@@ -22,7 +22,7 @@ import{S as Se,i as he,s as Re,e as r,w as P,b as v,c as ve,f as b,g as d,h as n
|
||||
'NEW_PASSWORD',
|
||||
'NEW_PASSWORD_CONFIRM',
|
||||
);
|
||||
`}});let I=o[2];const ie=e=>e[5].code;for(let e=0;e<I.length;e+=1){let t=be(o,I,e),c=ie(t);ae.set(c,w[e]=_e(c,t))}let y=o[2];const ce=e=>e[5].code;for(let e=0;e<y.length;e+=1){let t=ue(o,y,e),c=ce(t);ne.set(c,k[e]=ke(c,t))}return{c(){s=r("h3"),l=P("Confirm password reset ("),_=P(a),u=P(")"),i=v(),p=r("div"),m=r("p"),S=P("Confirms "),h=r("strong"),j=P(M),ee=P(" password reset request and sets a new password."),H=v(),ve(R.$$.fragment),L=v(),W=r("h6"),W.textContent="API details",z=v(),O=r("div"),q=r("strong"),q.textContent="POST",te=v(),B=r("div"),$=r("p"),se=P("/api/collections/"),G=r("strong"),J=P(F),le=P("/confirm-password-reset"),Q=v(),E=r("div"),E.textContent="Body Parameters",V=v(),T=r("table"),T.innerHTML=`<thead><tr><th>Param</th>
|
||||
`}});let I=o[2];const ie=e=>e[5].code;for(let e=0;e<I.length;e+=1){let t=be(o,I,e),c=ie(t);ae.set(c,w[e]=_e(c,t))}let y=o[2];const ce=e=>e[5].code;for(let e=0;e<y.length;e+=1){let t=ue(o,y,e),c=ce(t);ne.set(c,k[e]=ke(c,t))}return{c(){s=r("h3"),l=P("Confirm password reset ("),_=P(a),u=P(")"),i=v(),p=r("div"),m=r("p"),S=P("Confirms "),h=r("strong"),j=P(M),ee=P(" password reset request and sets a new password."),H=v(),ve(R.$$.fragment),L=v(),W=r("h6"),W.textContent="API details",Q=v(),N=r("div"),q=r("strong"),q.textContent="POST",te=v(),B=r("div"),$=r("p"),se=P("/api/collections/"),z=r("strong"),G=P(F),le=P("/confirm-password-reset"),J=v(),E=r("div"),E.textContent="Body Parameters",V=v(),T=r("table"),T.innerHTML=`<thead><tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th></tr></thead>
|
||||
<tbody><tr><td><div class="inline-flex"><span class="label label-success">Required</span>
|
||||
@@ -36,7 +36,7 @@ import{S as Se,i as he,s as Re,e as r,w as P,b as v,c as ve,f as b,g as d,h as n
|
||||
<tr><td><div class="inline-flex"><span class="label label-success">Required</span>
|
||||
<span>passwordConfirm</span></div></td>
|
||||
<td><span class="label">String</span></td>
|
||||
<td>The new password confirmation.</td></tr></tbody>`,X=v(),g=r("div"),g.textContent="Responses",Y=v(),N=r("div"),A=r("div");for(let e=0;e<w.length;e+=1)w[e].c();oe=v(),D=r("div");for(let e=0;e<k.length;e+=1)k[e].c();b(s,"class","m-b-sm"),b(p,"class","content txt-lg m-b-sm"),b(W,"class","m-b-xs"),b(q,"class","label label-primary"),b(B,"class","content"),b(O,"class","alert alert-success"),b(E,"class","section-title"),b(T,"class","table-compact table-border m-b-base"),b(g,"class","section-title"),b(A,"class","tabs-header compact left"),b(D,"class","tabs-content"),b(N,"class","tabs")},m(e,t){d(e,s,t),n(s,l),n(s,_),n(s,u),d(e,i,t),d(e,p,t),n(p,m),n(m,S),n(m,h),n(h,j),n(m,ee),d(e,H,t),we(R,e,t),d(e,L,t),d(e,W,t),d(e,z,t),d(e,O,t),n(O,q),n(O,te),n(O,B),n(B,$),n($,se),n($,G),n(G,J),n($,le),d(e,Q,t),d(e,E,t),d(e,V,t),d(e,T,t),d(e,X,t),d(e,g,t),d(e,Y,t),d(e,N,t),n(N,A);for(let c=0;c<w.length;c+=1)w[c]&&w[c].m(A,null);n(N,oe),n(N,D);for(let c=0;c<k.length;c+=1)k[c]&&k[c].m(D,null);C=!0},p(e,[t]){var fe,pe;(!C||t&1)&&a!==(a=e[0].name+"")&&K(_,a),(!C||t&1)&&M!==(M=e[0].name+"")&&K(j,M);const c={};t&9&&(c.js=`
|
||||
<td>The new password confirmation.</td></tr></tbody>`,X=v(),g=r("div"),g.textContent="Responses",Y=v(),O=r("div"),A=r("div");for(let e=0;e<w.length;e+=1)w[e].c();oe=v(),D=r("div");for(let e=0;e<k.length;e+=1)k[e].c();b(s,"class","m-b-sm"),b(p,"class","content txt-lg m-b-sm"),b(W,"class","m-b-xs"),b(q,"class","label label-primary"),b(B,"class","content"),b(N,"class","alert alert-success"),b(E,"class","section-title"),b(T,"class","table-compact table-border m-b-base"),b(g,"class","section-title"),b(A,"class","tabs-header compact left"),b(D,"class","tabs-content"),b(O,"class","tabs")},m(e,t){d(e,s,t),n(s,l),n(s,_),n(s,u),d(e,i,t),d(e,p,t),n(p,m),n(m,S),n(m,h),n(h,j),n(m,ee),d(e,H,t),we(R,e,t),d(e,L,t),d(e,W,t),d(e,Q,t),d(e,N,t),n(N,q),n(N,te),n(N,B),n(B,$),n($,se),n($,z),n(z,G),n($,le),d(e,J,t),d(e,E,t),d(e,V,t),d(e,T,t),d(e,X,t),d(e,g,t),d(e,Y,t),d(e,O,t),n(O,A);for(let c=0;c<w.length;c+=1)w[c]&&w[c].m(A,null);n(O,oe),n(O,D);for(let c=0;c<k.length;c+=1)k[c]&&k[c].m(D,null);C=!0},p(e,[t]){var fe,pe;(!C||t&1)&&a!==(a=e[0].name+"")&&K(_,a),(!C||t&1)&&M!==(M=e[0].name+"")&&K(j,M);const c={};t&9&&(c.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${e[3]}');
|
||||
@@ -60,7 +60,7 @@ import{S as Se,i as he,s as Re,e as r,w as P,b as v,c as ve,f as b,g as d,h as n
|
||||
'NEW_PASSWORD',
|
||||
'NEW_PASSWORD_CONFIRM',
|
||||
);
|
||||
`),R.$set(c),(!C||t&1)&&F!==(F=e[0].name+"")&&K(J,F),t&6&&(I=e[2],w=me(w,t,ie,1,e,I,ae,A,Oe,_e,null,be)),t&6&&(y=e[2],Ne(),k=me(k,t,ce,1,e,y,ne,D,Ce,ke,null,ue),We())},i(e){if(!C){Z(R.$$.fragment,e);for(let t=0;t<y.length;t+=1)Z(k[t]);C=!0}},o(e){x(R.$$.fragment,e);for(let t=0;t<k.length;t+=1)x(k[t]);C=!1},d(e){e&&f(s),e&&f(i),e&&f(p),e&&f(H),Pe(R,e),e&&f(L),e&&f(W),e&&f(z),e&&f(O),e&&f(Q),e&&f(E),e&&f(V),e&&f(T),e&&f(X),e&&f(g),e&&f(Y),e&&f(N);for(let t=0;t<w.length;t+=1)w[t].d();for(let t=0;t<k.length;t+=1)k[t].d()}}}function Me(o,s,l){let a,{collection:_=new $e}=s,u=204,i=[];const p=m=>l(1,u=m.code);return o.$$set=m=>{"collection"in m&&l(0,_=m.collection)},l(3,a=Ee.getApiExampleUrl(Te.baseUrl)),l(2,i=[{code:204,body:"null"},{code:400,body:`
|
||||
`),R.$set(c),(!C||t&1)&&F!==(F=e[0].name+"")&&K(G,F),t&6&&(I=e[2],w=me(w,t,ie,1,e,I,ae,A,Ne,_e,null,be)),t&6&&(y=e[2],Oe(),k=me(k,t,ce,1,e,y,ne,D,Ce,ke,null,ue),We())},i(e){if(!C){Z(R.$$.fragment,e);for(let t=0;t<y.length;t+=1)Z(k[t]);C=!0}},o(e){x(R.$$.fragment,e);for(let t=0;t<k.length;t+=1)x(k[t]);C=!1},d(e){e&&f(s),e&&f(i),e&&f(p),e&&f(H),Pe(R,e),e&&f(L),e&&f(W),e&&f(Q),e&&f(N),e&&f(J),e&&f(E),e&&f(V),e&&f(T),e&&f(X),e&&f(g),e&&f(Y),e&&f(O);for(let t=0;t<w.length;t+=1)w[t].d();for(let t=0;t<k.length;t+=1)k[t].d()}}}function Me(o,s,l){let a,{collection:_=new $e}=s,u=204,i=[];const p=m=>l(1,u=m.code);return o.$$set=m=>{"collection"in m&&l(0,_=m.collection)},l(3,a=Ee.getApiExampleUrl(Te.baseUrl)),l(2,i=[{code:204,body:"null"},{code:400,body:`
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to authenticate.",
|
||||
@@ -0,0 +1,62 @@
|
||||
import{S as Pe,i as Te,s as Se,e as r,w,b as k,c as ge,f as b,g as f,h as i,m as ye,x as D,N as _e,P as Be,k as qe,Q as Re,n as Ee,t as Z,a as x,o as p,d as Ce,T as Me,C as Ne,p as Oe,r as H,u as Ve,M as Ke}from"./index-6e04d404.js";import{S as Ae}from"./SdkTabs-1a82fff4.js";function ke(a,l,s){const o=a.slice();return o[5]=l[s],o}function ve(a,l,s){const o=a.slice();return o[5]=l[s],o}function we(a,l){let s,o=l[5].code+"",h,d,n,u;function m(){return l[4](l[5])}return{key:a,first:null,c(){s=r("button"),h=w(o),d=k(),b(s,"class","tab-item"),H(s,"active",l[1]===l[5].code),this.first=s},m($,g){f($,s,g),i(s,h),i(s,d),n||(u=Ve(s,"click",m),n=!0)},p($,g){l=$,g&4&&o!==(o=l[5].code+"")&&D(h,o),g&6&&H(s,"active",l[1]===l[5].code)},d($){$&&p(s),n=!1,u()}}}function $e(a,l){let s,o,h,d;return o=new Ke({props:{content:l[5].body}}),{key:a,first:null,c(){s=r("div"),ge(o.$$.fragment),h=k(),b(s,"class","tab-item"),H(s,"active",l[1]===l[5].code),this.first=s},m(n,u){f(n,s,u),ye(o,s,null),i(s,h),d=!0},p(n,u){l=n;const m={};u&4&&(m.content=l[5].body),o.$set(m),(!d||u&6)&&H(s,"active",l[1]===l[5].code)},i(n){d||(Z(o.$$.fragment,n),d=!0)},o(n){x(o.$$.fragment,n),d=!1},d(n){n&&p(s),Ce(o)}}}function Ue(a){var re,fe,pe,ue;let l,s,o=a[0].name+"",h,d,n,u,m,$,g,V=a[0].name+"",F,ee,I,y,L,S,Q,C,K,te,A,B,le,z,U=a[0].name+"",G,se,J,q,W,R,X,E,Y,P,M,v=[],oe=new Map,ae,N,_=[],ie=new Map,T;y=new Ae({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${a[3]}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${(re=a[0])==null?void 0:re.name}').confirmVerification('TOKEN');
|
||||
|
||||
// optionally refresh the previous authStore state with the latest record changes
|
||||
await pb.collection('${(fe=a[0])==null?void 0:fe.name}').authRefresh();
|
||||
`,dart:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${a[3]}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${(pe=a[0])==null?void 0:pe.name}').confirmVerification('TOKEN');
|
||||
|
||||
// optionally refresh the previous authStore state with the latest record changes
|
||||
await pb.collection('${(ue=a[0])==null?void 0:ue.name}').authRefresh();
|
||||
`}});let j=a[2];const ne=e=>e[5].code;for(let e=0;e<j.length;e+=1){let t=ve(a,j,e),c=ne(t);oe.set(c,v[e]=we(c,t))}let O=a[2];const ce=e=>e[5].code;for(let e=0;e<O.length;e+=1){let t=ke(a,O,e),c=ce(t);ie.set(c,_[e]=$e(c,t))}return{c(){l=r("h3"),s=w("Confirm verification ("),h=w(o),d=w(")"),n=k(),u=r("div"),m=r("p"),$=w("Confirms "),g=r("strong"),F=w(V),ee=w(" account verification request."),I=k(),ge(y.$$.fragment),L=k(),S=r("h6"),S.textContent="API details",Q=k(),C=r("div"),K=r("strong"),K.textContent="POST",te=k(),A=r("div"),B=r("p"),le=w("/api/collections/"),z=r("strong"),G=w(U),se=w("/confirm-verification"),J=k(),q=r("div"),q.textContent="Body Parameters",W=k(),R=r("table"),R.innerHTML=`<thead><tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th></tr></thead>
|
||||
<tbody><tr><td><div class="inline-flex"><span class="label label-success">Required</span>
|
||||
<span>token</span></div></td>
|
||||
<td><span class="label">String</span></td>
|
||||
<td>The token from the verification request email.</td></tr></tbody>`,X=k(),E=r("div"),E.textContent="Responses",Y=k(),P=r("div"),M=r("div");for(let e=0;e<v.length;e+=1)v[e].c();ae=k(),N=r("div");for(let e=0;e<_.length;e+=1)_[e].c();b(l,"class","m-b-sm"),b(u,"class","content txt-lg m-b-sm"),b(S,"class","m-b-xs"),b(K,"class","label label-primary"),b(A,"class","content"),b(C,"class","alert alert-success"),b(q,"class","section-title"),b(R,"class","table-compact table-border m-b-base"),b(E,"class","section-title"),b(M,"class","tabs-header compact left"),b(N,"class","tabs-content"),b(P,"class","tabs")},m(e,t){f(e,l,t),i(l,s),i(l,h),i(l,d),f(e,n,t),f(e,u,t),i(u,m),i(m,$),i(m,g),i(g,F),i(m,ee),f(e,I,t),ye(y,e,t),f(e,L,t),f(e,S,t),f(e,Q,t),f(e,C,t),i(C,K),i(C,te),i(C,A),i(A,B),i(B,le),i(B,z),i(z,G),i(B,se),f(e,J,t),f(e,q,t),f(e,W,t),f(e,R,t),f(e,X,t),f(e,E,t),f(e,Y,t),f(e,P,t),i(P,M);for(let c=0;c<v.length;c+=1)v[c]&&v[c].m(M,null);i(P,ae),i(P,N);for(let c=0;c<_.length;c+=1)_[c]&&_[c].m(N,null);T=!0},p(e,[t]){var me,de,be,he;(!T||t&1)&&o!==(o=e[0].name+"")&&D(h,o),(!T||t&1)&&V!==(V=e[0].name+"")&&D(F,V);const c={};t&9&&(c.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${(me=e[0])==null?void 0:me.name}').confirmVerification('TOKEN');
|
||||
|
||||
// optionally refresh the previous authStore state with the latest record changes
|
||||
await pb.collection('${(de=e[0])==null?void 0:de.name}').authRefresh();
|
||||
`),t&9&&(c.dart=`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${(be=e[0])==null?void 0:be.name}').confirmVerification('TOKEN');
|
||||
|
||||
// optionally refresh the previous authStore state with the latest record changes
|
||||
await pb.collection('${(he=e[0])==null?void 0:he.name}').authRefresh();
|
||||
`),y.$set(c),(!T||t&1)&&U!==(U=e[0].name+"")&&D(G,U),t&6&&(j=e[2],v=_e(v,t,ne,1,e,j,oe,M,Be,we,null,ve)),t&6&&(O=e[2],qe(),_=_e(_,t,ce,1,e,O,ie,N,Re,$e,null,ke),Ee())},i(e){if(!T){Z(y.$$.fragment,e);for(let t=0;t<O.length;t+=1)Z(_[t]);T=!0}},o(e){x(y.$$.fragment,e);for(let t=0;t<_.length;t+=1)x(_[t]);T=!1},d(e){e&&p(l),e&&p(n),e&&p(u),e&&p(I),Ce(y,e),e&&p(L),e&&p(S),e&&p(Q),e&&p(C),e&&p(J),e&&p(q),e&&p(W),e&&p(R),e&&p(X),e&&p(E),e&&p(Y),e&&p(P);for(let t=0;t<v.length;t+=1)v[t].d();for(let t=0;t<_.length;t+=1)_[t].d()}}}function je(a,l,s){let o,{collection:h=new Me}=l,d=204,n=[];const u=m=>s(1,d=m.code);return a.$$set=m=>{"collection"in m&&s(0,h=m.collection)},s(3,o=Ne.getApiExampleUrl(Oe.baseUrl)),s(2,n=[{code:204,body:"null"},{code:400,body:`
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to authenticate.",
|
||||
"data": {
|
||||
"token": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`}]),[h,d,n,o,u]}class Fe extends Pe{constructor(l){super(),Te(this,l,je,Ue,Se,{collection:0})}}export{Fe as default};
|
||||
@@ -1,50 +0,0 @@
|
||||
import{S as we,i as Ce,s as Pe,e as r,w as $,b as v,c as ve,f as b,g as f,h as n,m as he,x as H,N as de,O as Te,k as ge,P as ye,n as Be,t as Z,a as x,o as m,d as $e,T as qe,C as Oe,p as Se,r as R,u as Ee,M as Me}from"./index-79bb649c.js";import{S as Ne}from"./SdkTabs-22bd33d6.js";function ue(i,l,s){const o=i.slice();return o[5]=l[s],o}function be(i,l,s){const o=i.slice();return o[5]=l[s],o}function _e(i,l){let s,o=l[5].code+"",_,u,a,p;function d(){return l[4](l[5])}return{key:i,first:null,c(){s=r("button"),_=$(o),u=v(),b(s,"class","tab-item"),R(s,"active",l[1]===l[5].code),this.first=s},m(w,C){f(w,s,C),n(s,_),n(s,u),a||(p=Ee(s,"click",d),a=!0)},p(w,C){l=w,C&4&&o!==(o=l[5].code+"")&&H(_,o),C&6&&R(s,"active",l[1]===l[5].code)},d(w){w&&m(s),a=!1,p()}}}function ke(i,l){let s,o,_,u;return o=new Me({props:{content:l[5].body}}),{key:i,first:null,c(){s=r("div"),ve(o.$$.fragment),_=v(),b(s,"class","tab-item"),R(s,"active",l[1]===l[5].code),this.first=s},m(a,p){f(a,s,p),he(o,s,null),n(s,_),u=!0},p(a,p){l=a;const d={};p&4&&(d.content=l[5].body),o.$set(d),(!u||p&6)&&R(s,"active",l[1]===l[5].code)},i(a){u||(Z(o.$$.fragment,a),u=!0)},o(a){x(o.$$.fragment,a),u=!1},d(a){a&&m(s),$e(o)}}}function Ve(i){var re,fe;let l,s,o=i[0].name+"",_,u,a,p,d,w,C,K=i[0].name+"",F,ee,I,P,L,B,z,T,A,te,U,q,le,G,j=i[0].name+"",J,se,Q,O,W,S,X,E,Y,g,M,h=[],oe=new Map,ie,N,k=[],ne=new Map,y;P=new Ne({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${i[3]}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${(re=i[0])==null?void 0:re.name}').confirmVerification('TOKEN');
|
||||
`,dart:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${i[3]}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${(fe=i[0])==null?void 0:fe.name}').confirmVerification('TOKEN');
|
||||
`}});let D=i[2];const ae=e=>e[5].code;for(let e=0;e<D.length;e+=1){let t=be(i,D,e),c=ae(t);oe.set(c,h[e]=_e(c,t))}let V=i[2];const ce=e=>e[5].code;for(let e=0;e<V.length;e+=1){let t=ue(i,V,e),c=ce(t);ne.set(c,k[e]=ke(c,t))}return{c(){l=r("h3"),s=$("Confirm verification ("),_=$(o),u=$(")"),a=v(),p=r("div"),d=r("p"),w=$("Confirms "),C=r("strong"),F=$(K),ee=$(" account verification request."),I=v(),ve(P.$$.fragment),L=v(),B=r("h6"),B.textContent="API details",z=v(),T=r("div"),A=r("strong"),A.textContent="POST",te=v(),U=r("div"),q=r("p"),le=$("/api/collections/"),G=r("strong"),J=$(j),se=$("/confirm-verification"),Q=v(),O=r("div"),O.textContent="Body Parameters",W=v(),S=r("table"),S.innerHTML=`<thead><tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th></tr></thead>
|
||||
<tbody><tr><td><div class="inline-flex"><span class="label label-success">Required</span>
|
||||
<span>token</span></div></td>
|
||||
<td><span class="label">String</span></td>
|
||||
<td>The token from the verification request email.</td></tr></tbody>`,X=v(),E=r("div"),E.textContent="Responses",Y=v(),g=r("div"),M=r("div");for(let e=0;e<h.length;e+=1)h[e].c();ie=v(),N=r("div");for(let e=0;e<k.length;e+=1)k[e].c();b(l,"class","m-b-sm"),b(p,"class","content txt-lg m-b-sm"),b(B,"class","m-b-xs"),b(A,"class","label label-primary"),b(U,"class","content"),b(T,"class","alert alert-success"),b(O,"class","section-title"),b(S,"class","table-compact table-border m-b-base"),b(E,"class","section-title"),b(M,"class","tabs-header compact left"),b(N,"class","tabs-content"),b(g,"class","tabs")},m(e,t){f(e,l,t),n(l,s),n(l,_),n(l,u),f(e,a,t),f(e,p,t),n(p,d),n(d,w),n(d,C),n(C,F),n(d,ee),f(e,I,t),he(P,e,t),f(e,L,t),f(e,B,t),f(e,z,t),f(e,T,t),n(T,A),n(T,te),n(T,U),n(U,q),n(q,le),n(q,G),n(G,J),n(q,se),f(e,Q,t),f(e,O,t),f(e,W,t),f(e,S,t),f(e,X,t),f(e,E,t),f(e,Y,t),f(e,g,t),n(g,M);for(let c=0;c<h.length;c+=1)h[c]&&h[c].m(M,null);n(g,ie),n(g,N);for(let c=0;c<k.length;c+=1)k[c]&&k[c].m(N,null);y=!0},p(e,[t]){var me,pe;(!y||t&1)&&o!==(o=e[0].name+"")&&H(_,o),(!y||t&1)&&K!==(K=e[0].name+"")&&H(F,K);const c={};t&9&&(c.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${(me=e[0])==null?void 0:me.name}').confirmVerification('TOKEN');
|
||||
`),t&9&&(c.dart=`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${(pe=e[0])==null?void 0:pe.name}').confirmVerification('TOKEN');
|
||||
`),P.$set(c),(!y||t&1)&&j!==(j=e[0].name+"")&&H(J,j),t&6&&(D=e[2],h=de(h,t,ae,1,e,D,oe,M,Te,_e,null,be)),t&6&&(V=e[2],ge(),k=de(k,t,ce,1,e,V,ne,N,ye,ke,null,ue),Be())},i(e){if(!y){Z(P.$$.fragment,e);for(let t=0;t<V.length;t+=1)Z(k[t]);y=!0}},o(e){x(P.$$.fragment,e);for(let t=0;t<k.length;t+=1)x(k[t]);y=!1},d(e){e&&m(l),e&&m(a),e&&m(p),e&&m(I),$e(P,e),e&&m(L),e&&m(B),e&&m(z),e&&m(T),e&&m(Q),e&&m(O),e&&m(W),e&&m(S),e&&m(X),e&&m(E),e&&m(Y),e&&m(g);for(let t=0;t<h.length;t+=1)h[t].d();for(let t=0;t<k.length;t+=1)k[t].d()}}}function Ke(i,l,s){let o,{collection:_=new qe}=l,u=204,a=[];const p=d=>s(1,u=d.code);return i.$$set=d=>{"collection"in d&&s(0,_=d.collection)},s(3,o=Oe.getApiExampleUrl(Se.baseUrl)),s(2,a=[{code:204,body:"null"},{code:400,body:`
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to authenticate.",
|
||||
"data": {
|
||||
"token": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`}]),[_,u,a,o,p]}class je extends we{constructor(l){super(),Ce(this,l,Ke,Ve,Pe,{collection:0})}}export{je as default};
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import{S as Pt,i as gt,s as Ft,C as Q,M as Lt,e as a,w as k,b as m,c as be,f as h,g as r,h as n,m as _e,x,N as Be,P as $t,k as Bt,Q as Rt,n as jt,t as fe,a as pe,o as d,d as ke,T as Dt,p as Nt,r as ye,u as Vt,y as ne}from"./index-6e04d404.js";import{S as Jt}from"./SdkTabs-1a82fff4.js";import{F as Et}from"./FieldsQueryParam-db646c6c.js";function Ct(o,e,l){const s=o.slice();return s[7]=e[l],s}function St(o,e,l){const s=o.slice();return s[7]=e[l],s}function Tt(o,e,l){const s=o.slice();return s[12]=e[l],s}function qt(o){let e;return{c(){e=a("p"),e.innerHTML="Requires admin <code>Authorization:TOKEN</code> header",h(e,"class","txt-hint txt-sm txt-right")},m(l,s){r(l,e,s)},d(l){l&&d(e)}}}function Mt(o){let e,l,s,b,p,c,f,v,T,w,M,R,D,E,L,I,j,B,C,N,q,$,_;function O(u,S){var ee,K;return(K=(ee=u[0])==null?void 0:ee.options)!=null&&K.requireEmail?Ut:It}let z=O(o),P=z(o);return{c(){e=a("tr"),e.innerHTML='<td colspan="3" class="txt-hint">Auth fields</td>',l=m(),s=a("tr"),s.innerHTML=`<td><div class="inline-flex"><span class="label label-warning">Optional</span>
|
||||
<span>username</span></div></td>
|
||||
<td><span class="label">String</span></td>
|
||||
<td>The username of the auth record.
|
||||
<br/>
|
||||
If not set, it will be auto generated.</td>`,b=m(),p=a("tr"),c=a("td"),f=a("div"),P.c(),v=m(),T=a("span"),T.textContent="email",w=m(),M=a("td"),M.innerHTML='<span class="label">String</span>',R=m(),D=a("td"),D.textContent="Auth record email address.",E=m(),L=a("tr"),L.innerHTML=`<td><div class="inline-flex"><span class="label label-warning">Optional</span>
|
||||
<span>emailVisibility</span></div></td>
|
||||
<td><span class="label">Boolean</span></td>
|
||||
<td>Whether to show/hide the auth record email when fetching the record data.</td>`,I=m(),j=a("tr"),j.innerHTML=`<td><div class="inline-flex"><span class="label label-success">Required</span>
|
||||
<span>password</span></div></td>
|
||||
<td><span class="label">String</span></td>
|
||||
<td>Auth record password.</td>`,B=m(),C=a("tr"),C.innerHTML=`<td><div class="inline-flex"><span class="label label-success">Required</span>
|
||||
<span>passwordConfirm</span></div></td>
|
||||
<td><span class="label">String</span></td>
|
||||
<td>Auth record password confirmation.</td>`,N=m(),q=a("tr"),q.innerHTML=`<td><div class="inline-flex"><span class="label label-warning">Optional</span>
|
||||
<span>verified</span></div></td>
|
||||
<td><span class="label">Boolean</span></td>
|
||||
<td>Indicates whether the auth record is verified or not.
|
||||
<br/>
|
||||
This field can be set only by admins or auth records with "Manage" access.</td>`,$=m(),_=a("tr"),_.innerHTML='<td colspan="3" class="txt-hint">Schema fields</td>',h(f,"class","inline-flex")},m(u,S){r(u,e,S),r(u,l,S),r(u,s,S),r(u,b,S),r(u,p,S),n(p,c),n(c,f),P.m(f,null),n(f,v),n(f,T),n(p,w),n(p,M),n(p,R),n(p,D),r(u,E,S),r(u,L,S),r(u,I,S),r(u,j,S),r(u,B,S),r(u,C,S),r(u,N,S),r(u,q,S),r(u,$,S),r(u,_,S)},p(u,S){z!==(z=O(u))&&(P.d(1),P=z(u),P&&(P.c(),P.m(f,v)))},d(u){u&&d(e),u&&d(l),u&&d(s),u&&d(b),u&&d(p),P.d(),u&&d(E),u&&d(L),u&&d(I),u&&d(j),u&&d(B),u&&d(C),u&&d(N),u&&d(q),u&&d($),u&&d(_)}}}function It(o){let e;return{c(){e=a("span"),e.textContent="Optional",h(e,"class","label label-warning")},m(l,s){r(l,e,s)},d(l){l&&d(e)}}}function Ut(o){let e;return{c(){e=a("span"),e.textContent="Required",h(e,"class","label label-success")},m(l,s){r(l,e,s)},d(l){l&&d(e)}}}function Qt(o){let e;return{c(){e=a("span"),e.textContent="Optional",h(e,"class","label label-warning")},m(l,s){r(l,e,s)},d(l){l&&d(e)}}}function zt(o){let e;return{c(){e=a("span"),e.textContent="Required",h(e,"class","label label-success")},m(l,s){r(l,e,s)},d(l){l&&d(e)}}}function Kt(o){var p;let e,l=((p=o[12].options)==null?void 0:p.maxSelect)===1?"id":"ids",s,b;return{c(){e=k("Relation record "),s=k(l),b=k(".")},m(c,f){r(c,e,f),r(c,s,f),r(c,b,f)},p(c,f){var v;f&1&&l!==(l=((v=c[12].options)==null?void 0:v.maxSelect)===1?"id":"ids")&&x(s,l)},d(c){c&&d(e),c&&d(s),c&&d(b)}}}function Wt(o){let e,l,s,b,p;return{c(){e=k("File object."),l=a("br"),s=k(`
|
||||
Set to `),b=a("code"),b.textContent="null",p=k(" to delete already uploaded file(s).")},m(c,f){r(c,e,f),r(c,l,f),r(c,s,f),r(c,b,f),r(c,p,f)},p:ne,d(c){c&&d(e),c&&d(l),c&&d(s),c&&d(b),c&&d(p)}}}function Yt(o){let e;return{c(){e=k("URL address.")},m(l,s){r(l,e,s)},p:ne,d(l){l&&d(e)}}}function Gt(o){let e;return{c(){e=k("Email address.")},m(l,s){r(l,e,s)},p:ne,d(l){l&&d(e)}}}function Xt(o){let e;return{c(){e=k("JSON array or object.")},m(l,s){r(l,e,s)},p:ne,d(l){l&&d(e)}}}function Zt(o){let e;return{c(){e=k("Number value.")},m(l,s){r(l,e,s)},p:ne,d(l){l&&d(e)}}}function xt(o){let e;return{c(){e=k("Plain text value.")},m(l,s){r(l,e,s)},p:ne,d(l){l&&d(e)}}}function Ot(o,e){let l,s,b,p,c,f=e[12].name+"",v,T,w,M,R=Q.getFieldValueType(e[12])+"",D,E,L,I;function j(_,O){return _[12].required?zt:Qt}let B=j(e),C=B(e);function N(_,O){if(_[12].type==="text")return xt;if(_[12].type==="number")return Zt;if(_[12].type==="json")return Xt;if(_[12].type==="email")return Gt;if(_[12].type==="url")return Yt;if(_[12].type==="file")return Wt;if(_[12].type==="relation")return Kt}let q=N(e),$=q&&q(e);return{key:o,first:null,c(){l=a("tr"),s=a("td"),b=a("div"),C.c(),p=m(),c=a("span"),v=k(f),T=m(),w=a("td"),M=a("span"),D=k(R),E=m(),L=a("td"),$&&$.c(),I=m(),h(b,"class","inline-flex"),h(M,"class","label"),this.first=l},m(_,O){r(_,l,O),n(l,s),n(s,b),C.m(b,null),n(b,p),n(b,c),n(c,v),n(l,T),n(l,w),n(w,M),n(M,D),n(l,E),n(l,L),$&&$.m(L,null),n(l,I)},p(_,O){e=_,B!==(B=j(e))&&(C.d(1),C=B(e),C&&(C.c(),C.m(b,p))),O&1&&f!==(f=e[12].name+"")&&x(v,f),O&1&&R!==(R=Q.getFieldValueType(e[12])+"")&&x(D,R),q===(q=N(e))&&$?$.p(e,O):($&&$.d(1),$=q&&q(e),$&&($.c(),$.m(L,null)))},d(_){_&&d(l),C.d(),$&&$.d()}}}function At(o,e){let l,s=e[7].code+"",b,p,c,f;function v(){return e[6](e[7])}return{key:o,first:null,c(){l=a("button"),b=k(s),p=m(),h(l,"class","tab-item"),ye(l,"active",e[1]===e[7].code),this.first=l},m(T,w){r(T,l,w),n(l,b),n(l,p),c||(f=Vt(l,"click",v),c=!0)},p(T,w){e=T,w&4&&s!==(s=e[7].code+"")&&x(b,s),w&6&&ye(l,"active",e[1]===e[7].code)},d(T){T&&d(l),c=!1,f()}}}function Ht(o,e){let l,s,b,p;return s=new Lt({props:{content:e[7].body}}),{key:o,first:null,c(){l=a("div"),be(s.$$.fragment),b=m(),h(l,"class","tab-item"),ye(l,"active",e[1]===e[7].code),this.first=l},m(c,f){r(c,l,f),_e(s,l,null),n(l,b),p=!0},p(c,f){e=c;const v={};f&4&&(v.content=e[7].body),s.$set(v),(!p||f&6)&&ye(l,"active",e[1]===e[7].code)},i(c){p||(fe(s.$$.fragment,c),p=!0)},o(c){pe(s.$$.fragment,c),p=!1},d(c){c&&d(l),ke(s)}}}function el(o){var at,ot,rt,dt,ct,ft,pt,ut;let e,l,s=o[0].name+"",b,p,c,f,v,T,w,M=o[0].name+"",R,D,E,L,I,j,B,C,N,q,$,_,O,z,P,u,S,ee,K=o[0].name+"",ve,Re,je,he,se,we,W,$e,De,U,Ce,Ne,Se,V=[],Ve=new Map,Te,ie,qe,Y,Me,Je,ae,G,Oe,Ee,Ae,Ie,A,Ue,te,Qe,ze,Ke,He,We,Le,Ye,Ge,Xe,Pe,Ze,xe,le,ge,oe,Fe,X,re,J=[],et=new Map,tt,de,g=[],lt=new Map,Z;C=new Jt({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${o[4]}');
|
||||
|
||||
...
|
||||
|
||||
// example create data
|
||||
const data = ${JSON.stringify(Object.assign({},o[3],Q.dummyCollectionSchemaData(o[0])),null,4)};
|
||||
|
||||
const record = await pb.collection('${(at=o[0])==null?void 0:at.name}').create(data);
|
||||
`+((ot=o[0])!=null&&ot.isAuth?`
|
||||
// (optional) send an email verification request
|
||||
await pb.collection('${(rt=o[0])==null?void 0:rt.name}').requestVerification('test@example.com');
|
||||
`:""),dart:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${o[4]}');
|
||||
|
||||
...
|
||||
|
||||
// example create body
|
||||
final body = <String, dynamic>${JSON.stringify(Object.assign({},o[3],Q.dummyCollectionSchemaData(o[0])),null,2)};
|
||||
|
||||
final record = await pb.collection('${(dt=o[0])==null?void 0:dt.name}').create(body: body);
|
||||
`+((ct=o[0])!=null&&ct.isAuth?`
|
||||
// (optional) send an email verification request
|
||||
await pb.collection('${(ft=o[0])==null?void 0:ft.name}').requestVerification('test@example.com');
|
||||
`:"")}});let F=o[5]&&qt(),H=((pt=o[0])==null?void 0:pt.isAuth)&&Mt(o),ue=(ut=o[0])==null?void 0:ut.schema;const nt=t=>t[12].name;for(let t=0;t<ue.length;t+=1){let i=Tt(o,ue,t),y=nt(i);Ve.set(y,V[t]=Ot(y,i))}te=new Lt({props:{content:"?expand=relField1,relField2.subRelField"}}),le=new Et({});let me=o[2];const st=t=>t[7].code;for(let t=0;t<me.length;t+=1){let i=St(o,me,t),y=st(i);et.set(y,J[t]=At(y,i))}let ce=o[2];const it=t=>t[7].code;for(let t=0;t<ce.length;t+=1){let i=Ct(o,ce,t),y=it(i);lt.set(y,g[t]=Ht(y,i))}return{c(){e=a("h3"),l=k("Create ("),b=k(s),p=k(")"),c=m(),f=a("div"),v=a("p"),T=k("Create a new "),w=a("strong"),R=k(M),D=k(" record."),E=m(),L=a("p"),L.innerHTML=`Body parameters could be sent as <code>application/json</code> or
|
||||
<code>multipart/form-data</code>.`,I=m(),j=a("p"),j.innerHTML=`File upload is supported only via <code>multipart/form-data</code>.
|
||||
<br/>
|
||||
For more info and examples you could check the detailed
|
||||
<a href="https://pocketbase.io/docs/files-handling/" target="_blank" rel="noopener noreferrer">Files upload and handling docs
|
||||
</a>.`,B=m(),be(C.$$.fragment),N=m(),q=a("h6"),q.textContent="API details",$=m(),_=a("div"),O=a("strong"),O.textContent="POST",z=m(),P=a("div"),u=a("p"),S=k("/api/collections/"),ee=a("strong"),ve=k(K),Re=k("/records"),je=m(),F&&F.c(),he=m(),se=a("div"),se.textContent="Body Parameters",we=m(),W=a("table"),$e=a("thead"),$e.innerHTML=`<tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th></tr>`,De=m(),U=a("tbody"),Ce=a("tr"),Ce.innerHTML=`<td><div class="inline-flex"><span class="label label-warning">Optional</span>
|
||||
<span>id</span></div></td>
|
||||
<td><span class="label">String</span></td>
|
||||
<td><strong>15 characters string</strong> to store as record ID.
|
||||
<br/>
|
||||
If not set, it will be auto generated.</td>`,Ne=m(),H&&H.c(),Se=m();for(let t=0;t<V.length;t+=1)V[t].c();Te=m(),ie=a("div"),ie.textContent="Query parameters",qe=m(),Y=a("table"),Me=a("thead"),Me.innerHTML=`<tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th></tr>`,Je=m(),ae=a("tbody"),G=a("tr"),Oe=a("td"),Oe.textContent="expand",Ee=m(),Ae=a("td"),Ae.innerHTML='<span class="label">String</span>',Ie=m(),A=a("td"),Ue=k(`Auto expand relations when returning the created record. Ex.:
|
||||
`),be(te.$$.fragment),Qe=k(`
|
||||
Supports up to 6-levels depth nested relations expansion. `),ze=a("br"),Ke=k(`
|
||||
The expanded relations will be appended to the record under the
|
||||
`),He=a("code"),He.textContent="expand",We=k(" property (eg. "),Le=a("code"),Le.textContent='"expand": {"relField1": {...}, ...}',Ye=k(`).
|
||||
`),Ge=a("br"),Xe=k(`
|
||||
Only the relations to which the request user has permissions to `),Pe=a("strong"),Pe.textContent="view",Ze=k(" will be expanded."),xe=m(),be(le.$$.fragment),ge=m(),oe=a("div"),oe.textContent="Responses",Fe=m(),X=a("div"),re=a("div");for(let t=0;t<J.length;t+=1)J[t].c();tt=m(),de=a("div");for(let t=0;t<g.length;t+=1)g[t].c();h(e,"class","m-b-sm"),h(f,"class","content txt-lg m-b-sm"),h(q,"class","m-b-xs"),h(O,"class","label label-primary"),h(P,"class","content"),h(_,"class","alert alert-success"),h(se,"class","section-title"),h(W,"class","table-compact table-border m-b-base"),h(ie,"class","section-title"),h(Y,"class","table-compact table-border m-b-base"),h(oe,"class","section-title"),h(re,"class","tabs-header compact left"),h(de,"class","tabs-content"),h(X,"class","tabs")},m(t,i){r(t,e,i),n(e,l),n(e,b),n(e,p),r(t,c,i),r(t,f,i),n(f,v),n(v,T),n(v,w),n(w,R),n(v,D),n(f,E),n(f,L),n(f,I),n(f,j),r(t,B,i),_e(C,t,i),r(t,N,i),r(t,q,i),r(t,$,i),r(t,_,i),n(_,O),n(_,z),n(_,P),n(P,u),n(u,S),n(u,ee),n(ee,ve),n(u,Re),n(_,je),F&&F.m(_,null),r(t,he,i),r(t,se,i),r(t,we,i),r(t,W,i),n(W,$e),n(W,De),n(W,U),n(U,Ce),n(U,Ne),H&&H.m(U,null),n(U,Se);for(let y=0;y<V.length;y+=1)V[y]&&V[y].m(U,null);r(t,Te,i),r(t,ie,i),r(t,qe,i),r(t,Y,i),n(Y,Me),n(Y,Je),n(Y,ae),n(ae,G),n(G,Oe),n(G,Ee),n(G,Ae),n(G,Ie),n(G,A),n(A,Ue),_e(te,A,null),n(A,Qe),n(A,ze),n(A,Ke),n(A,He),n(A,We),n(A,Le),n(A,Ye),n(A,Ge),n(A,Xe),n(A,Pe),n(A,Ze),n(ae,xe),_e(le,ae,null),r(t,ge,i),r(t,oe,i),r(t,Fe,i),r(t,X,i),n(X,re);for(let y=0;y<J.length;y+=1)J[y]&&J[y].m(re,null);n(X,tt),n(X,de);for(let y=0;y<g.length;y+=1)g[y]&&g[y].m(de,null);Z=!0},p(t,[i]){var mt,bt,_t,kt,yt,vt,ht,wt;(!Z||i&1)&&s!==(s=t[0].name+"")&&x(b,s),(!Z||i&1)&&M!==(M=t[0].name+"")&&x(R,M);const y={};i&25&&(y.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${t[4]}');
|
||||
|
||||
...
|
||||
|
||||
// example create data
|
||||
const data = ${JSON.stringify(Object.assign({},t[3],Q.dummyCollectionSchemaData(t[0])),null,4)};
|
||||
|
||||
const record = await pb.collection('${(mt=t[0])==null?void 0:mt.name}').create(data);
|
||||
`+((bt=t[0])!=null&&bt.isAuth?`
|
||||
// (optional) send an email verification request
|
||||
await pb.collection('${(_t=t[0])==null?void 0:_t.name}').requestVerification('test@example.com');
|
||||
`:"")),i&25&&(y.dart=`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${t[4]}');
|
||||
|
||||
...
|
||||
|
||||
// example create body
|
||||
final body = <String, dynamic>${JSON.stringify(Object.assign({},t[3],Q.dummyCollectionSchemaData(t[0])),null,2)};
|
||||
|
||||
final record = await pb.collection('${(kt=t[0])==null?void 0:kt.name}').create(body: body);
|
||||
`+((yt=t[0])!=null&&yt.isAuth?`
|
||||
// (optional) send an email verification request
|
||||
await pb.collection('${(vt=t[0])==null?void 0:vt.name}').requestVerification('test@example.com');
|
||||
`:"")),C.$set(y),(!Z||i&1)&&K!==(K=t[0].name+"")&&x(ve,K),t[5]?F||(F=qt(),F.c(),F.m(_,null)):F&&(F.d(1),F=null),(ht=t[0])!=null&&ht.isAuth?H?H.p(t,i):(H=Mt(t),H.c(),H.m(U,Se)):H&&(H.d(1),H=null),i&1&&(ue=(wt=t[0])==null?void 0:wt.schema,V=Be(V,i,nt,1,t,ue,Ve,U,$t,Ot,null,Tt)),i&6&&(me=t[2],J=Be(J,i,st,1,t,me,et,re,$t,At,null,St)),i&6&&(ce=t[2],Bt(),g=Be(g,i,it,1,t,ce,lt,de,Rt,Ht,null,Ct),jt())},i(t){if(!Z){fe(C.$$.fragment,t),fe(te.$$.fragment,t),fe(le.$$.fragment,t);for(let i=0;i<ce.length;i+=1)fe(g[i]);Z=!0}},o(t){pe(C.$$.fragment,t),pe(te.$$.fragment,t),pe(le.$$.fragment,t);for(let i=0;i<g.length;i+=1)pe(g[i]);Z=!1},d(t){t&&d(e),t&&d(c),t&&d(f),t&&d(B),ke(C,t),t&&d(N),t&&d(q),t&&d($),t&&d(_),F&&F.d(),t&&d(he),t&&d(se),t&&d(we),t&&d(W),H&&H.d();for(let i=0;i<V.length;i+=1)V[i].d();t&&d(Te),t&&d(ie),t&&d(qe),t&&d(Y),ke(te),ke(le),t&&d(ge),t&&d(oe),t&&d(Fe),t&&d(X);for(let i=0;i<J.length;i+=1)J[i].d();for(let i=0;i<g.length;i+=1)g[i].d()}}}function tl(o,e,l){let s,b,{collection:p=new Dt}=e,c=200,f=[],v={};const T=w=>l(1,c=w.code);return o.$$set=w=>{"collection"in w&&l(0,p=w.collection)},o.$$.update=()=>{var w,M;o.$$.dirty&1&&l(5,s=(p==null?void 0:p.createRule)===null),o.$$.dirty&1&&l(2,f=[{code:200,body:JSON.stringify(Q.dummyCollectionRecord(p),null,2)},{code:400,body:`
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to create record.",
|
||||
"data": {
|
||||
"${(M=(w=p==null?void 0:p.schema)==null?void 0:w[0])==null?void 0:M.name}": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`},{code:403,body:`
|
||||
{
|
||||
"code": 403,
|
||||
"message": "You are not allowed to perform this request.",
|
||||
"data": {}
|
||||
}
|
||||
`}]),o.$$.dirty&1&&(p.$isAuth?l(3,v={username:"test_username",email:"test@example.com",emailVisibility:!0,password:"12345678",passwordConfirm:"12345678"}):l(3,v={}))},l(4,b=Q.getApiExampleUrl(Nt.baseUrl)),[p,c,f,v,b,s,T]}class il extends Pt{constructor(e){super(),gt(this,e,tl,el,Ft,{collection:0})}}export{il as default};
|
||||
-114
@@ -1,114 +0,0 @@
|
||||
import{S as Ht,i as Lt,s as Pt,C as z,M as At,e as a,w as k,b as m,c as Pe,f as h,g as r,h as n,m as Be,x,N as Le,O as ht,k as Bt,P as Ft,n as Rt,t as fe,a as pe,o as d,d as Fe,T as gt,p as jt,r as ue,u as Dt,y as le}from"./index-79bb649c.js";import{S as Nt}from"./SdkTabs-22bd33d6.js";function wt(o,e,l){const s=o.slice();return s[7]=e[l],s}function Ct(o,e,l){const s=o.slice();return s[7]=e[l],s}function St(o,e,l){const s=o.slice();return s[12]=e[l],s}function $t(o){let e;return{c(){e=a("p"),e.innerHTML="Requires admin <code>Authorization:TOKEN</code> header",h(e,"class","txt-hint txt-sm txt-right")},m(l,s){r(l,e,s)},d(l){l&&d(e)}}}function Tt(o){let e,l,s,b,p,c,f,v,T,w,M,g,D,E,L,I,j,R,S,N,q,C,_;function O(u,$){var ee,Q;return(Q=(ee=u[0])==null?void 0:ee.options)!=null&&Q.requireEmail?Jt:Vt}let K=O(o),P=K(o);return{c(){e=a("tr"),e.innerHTML='<td colspan="3" class="txt-hint">Auth fields</td>',l=m(),s=a("tr"),s.innerHTML=`<td><div class="inline-flex"><span class="label label-warning">Optional</span>
|
||||
<span>username</span></div></td>
|
||||
<td><span class="label">String</span></td>
|
||||
<td>The username of the auth record.
|
||||
<br/>
|
||||
If not set, it will be auto generated.</td>`,b=m(),p=a("tr"),c=a("td"),f=a("div"),P.c(),v=m(),T=a("span"),T.textContent="email",w=m(),M=a("td"),M.innerHTML='<span class="label">String</span>',g=m(),D=a("td"),D.textContent="Auth record email address.",E=m(),L=a("tr"),L.innerHTML=`<td><div class="inline-flex"><span class="label label-warning">Optional</span>
|
||||
<span>emailVisibility</span></div></td>
|
||||
<td><span class="label">Boolean</span></td>
|
||||
<td>Whether to show/hide the auth record email when fetching the record data.</td>`,I=m(),j=a("tr"),j.innerHTML=`<td><div class="inline-flex"><span class="label label-success">Required</span>
|
||||
<span>password</span></div></td>
|
||||
<td><span class="label">String</span></td>
|
||||
<td>Auth record password.</td>`,R=m(),S=a("tr"),S.innerHTML=`<td><div class="inline-flex"><span class="label label-success">Required</span>
|
||||
<span>passwordConfirm</span></div></td>
|
||||
<td><span class="label">String</span></td>
|
||||
<td>Auth record password confirmation.</td>`,N=m(),q=a("tr"),q.innerHTML=`<td><div class="inline-flex"><span class="label label-warning">Optional</span>
|
||||
<span>verified</span></div></td>
|
||||
<td><span class="label">Boolean</span></td>
|
||||
<td>Indicates whether the auth record is verified or not.
|
||||
<br/>
|
||||
This field can be set only by admins or auth records with "Manage" access.</td>`,C=m(),_=a("tr"),_.innerHTML='<td colspan="3" class="txt-hint">Schema fields</td>',h(f,"class","inline-flex")},m(u,$){r(u,e,$),r(u,l,$),r(u,s,$),r(u,b,$),r(u,p,$),n(p,c),n(c,f),P.m(f,null),n(f,v),n(f,T),n(p,w),n(p,M),n(p,g),n(p,D),r(u,E,$),r(u,L,$),r(u,I,$),r(u,j,$),r(u,R,$),r(u,S,$),r(u,N,$),r(u,q,$),r(u,C,$),r(u,_,$)},p(u,$){K!==(K=O(u))&&(P.d(1),P=K(u),P&&(P.c(),P.m(f,v)))},d(u){u&&d(e),u&&d(l),u&&d(s),u&&d(b),u&&d(p),P.d(),u&&d(E),u&&d(L),u&&d(I),u&&d(j),u&&d(R),u&&d(S),u&&d(N),u&&d(q),u&&d(C),u&&d(_)}}}function Vt(o){let e;return{c(){e=a("span"),e.textContent="Optional",h(e,"class","label label-warning")},m(l,s){r(l,e,s)},d(l){l&&d(e)}}}function Jt(o){let e;return{c(){e=a("span"),e.textContent="Required",h(e,"class","label label-success")},m(l,s){r(l,e,s)},d(l){l&&d(e)}}}function Et(o){let e;return{c(){e=a("span"),e.textContent="Optional",h(e,"class","label label-warning")},m(l,s){r(l,e,s)},d(l){l&&d(e)}}}function It(o){let e;return{c(){e=a("span"),e.textContent="Required",h(e,"class","label label-success")},m(l,s){r(l,e,s)},d(l){l&&d(e)}}}function Ut(o){var p;let e,l=((p=o[12].options)==null?void 0:p.maxSelect)===1?"id":"ids",s,b;return{c(){e=k("Relation record "),s=k(l),b=k(".")},m(c,f){r(c,e,f),r(c,s,f),r(c,b,f)},p(c,f){var v;f&1&&l!==(l=((v=c[12].options)==null?void 0:v.maxSelect)===1?"id":"ids")&&x(s,l)},d(c){c&&d(e),c&&d(s),c&&d(b)}}}function zt(o){let e,l,s,b,p;return{c(){e=k("File object."),l=a("br"),s=k(`
|
||||
Set to `),b=a("code"),b.textContent="null",p=k(" to delete already uploaded file(s).")},m(c,f){r(c,e,f),r(c,l,f),r(c,s,f),r(c,b,f),r(c,p,f)},p:le,d(c){c&&d(e),c&&d(l),c&&d(s),c&&d(b),c&&d(p)}}}function Kt(o){let e;return{c(){e=k("URL address.")},m(l,s){r(l,e,s)},p:le,d(l){l&&d(e)}}}function Qt(o){let e;return{c(){e=k("Email address.")},m(l,s){r(l,e,s)},p:le,d(l){l&&d(e)}}}function Wt(o){let e;return{c(){e=k("JSON array or object.")},m(l,s){r(l,e,s)},p:le,d(l){l&&d(e)}}}function Yt(o){let e;return{c(){e=k("Number value.")},m(l,s){r(l,e,s)},p:le,d(l){l&&d(e)}}}function Gt(o){let e;return{c(){e=k("Plain text value.")},m(l,s){r(l,e,s)},p:le,d(l){l&&d(e)}}}function qt(o,e){let l,s,b,p,c,f=e[12].name+"",v,T,w,M,g=z.getFieldValueType(e[12])+"",D,E,L,I;function j(_,O){return _[12].required?It:Et}let R=j(e),S=R(e);function N(_,O){if(_[12].type==="text")return Gt;if(_[12].type==="number")return Yt;if(_[12].type==="json")return Wt;if(_[12].type==="email")return Qt;if(_[12].type==="url")return Kt;if(_[12].type==="file")return zt;if(_[12].type==="relation")return Ut}let q=N(e),C=q&&q(e);return{key:o,first:null,c(){l=a("tr"),s=a("td"),b=a("div"),S.c(),p=m(),c=a("span"),v=k(f),T=m(),w=a("td"),M=a("span"),D=k(g),E=m(),L=a("td"),C&&C.c(),I=m(),h(b,"class","inline-flex"),h(M,"class","label"),this.first=l},m(_,O){r(_,l,O),n(l,s),n(s,b),S.m(b,null),n(b,p),n(b,c),n(c,v),n(l,T),n(l,w),n(w,M),n(M,D),n(l,E),n(l,L),C&&C.m(L,null),n(l,I)},p(_,O){e=_,R!==(R=j(e))&&(S.d(1),S=R(e),S&&(S.c(),S.m(b,p))),O&1&&f!==(f=e[12].name+"")&&x(v,f),O&1&&g!==(g=z.getFieldValueType(e[12])+"")&&x(D,g),q===(q=N(e))&&C?C.p(e,O):(C&&C.d(1),C=q&&q(e),C&&(C.c(),C.m(L,null)))},d(_){_&&d(l),S.d(),C&&C.d()}}}function Mt(o,e){let l,s=e[7].code+"",b,p,c,f;function v(){return e[6](e[7])}return{key:o,first:null,c(){l=a("button"),b=k(s),p=m(),h(l,"class","tab-item"),ue(l,"active",e[1]===e[7].code),this.first=l},m(T,w){r(T,l,w),n(l,b),n(l,p),c||(f=Dt(l,"click",v),c=!0)},p(T,w){e=T,w&4&&s!==(s=e[7].code+"")&&x(b,s),w&6&&ue(l,"active",e[1]===e[7].code)},d(T){T&&d(l),c=!1,f()}}}function Ot(o,e){let l,s,b,p;return s=new At({props:{content:e[7].body}}),{key:o,first:null,c(){l=a("div"),Pe(s.$$.fragment),b=m(),h(l,"class","tab-item"),ue(l,"active",e[1]===e[7].code),this.first=l},m(c,f){r(c,l,f),Be(s,l,null),n(l,b),p=!0},p(c,f){e=c;const v={};f&4&&(v.content=e[7].body),s.$set(v),(!p||f&6)&&ue(l,"active",e[1]===e[7].code)},i(c){p||(fe(s.$$.fragment,c),p=!0)},o(c){pe(s.$$.fragment,c),p=!1},d(c){c&&d(l),Fe(s)}}}function Xt(o){var st,it,at,ot,rt,dt,ct,ft;let e,l,s=o[0].name+"",b,p,c,f,v,T,w,M=o[0].name+"",g,D,E,L,I,j,R,S,N,q,C,_,O,K,P,u,$,ee,Q=o[0].name+"",me,Re,ge,be,ne,_e,W,ke,je,U,ye,De,ve,V=[],Ne=new Map,he,se,we,Y,Ce,Ve,Se,G,$e,Je,Te,Ee,A,Ie,te,Ue,ze,Ke,qe,Qe,Me,We,Ye,Ge,Oe,Xe,Ae,ie,He,X,ae,J=[],Ze=new Map,xe,oe,B=[],et=new Map,Z;S=new Nt({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${o[4]}');
|
||||
|
||||
...
|
||||
|
||||
// example create data
|
||||
const data = ${JSON.stringify(Object.assign({},o[3],z.dummyCollectionSchemaData(o[0])),null,4)};
|
||||
|
||||
const record = await pb.collection('${(st=o[0])==null?void 0:st.name}').create(data);
|
||||
`+((it=o[0])!=null&&it.isAuth?`
|
||||
// (optional) send an email verification request
|
||||
await pb.collection('${(at=o[0])==null?void 0:at.name}').requestVerification('test@example.com');
|
||||
`:""),dart:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${o[4]}');
|
||||
|
||||
...
|
||||
|
||||
// example create body
|
||||
final body = <String, dynamic>${JSON.stringify(Object.assign({},o[3],z.dummyCollectionSchemaData(o[0])),null,2)};
|
||||
|
||||
final record = await pb.collection('${(ot=o[0])==null?void 0:ot.name}').create(body: body);
|
||||
`+((rt=o[0])!=null&&rt.isAuth?`
|
||||
// (optional) send an email verification request
|
||||
await pb.collection('${(dt=o[0])==null?void 0:dt.name}').requestVerification('test@example.com');
|
||||
`:"")}});let F=o[5]&&$t(),H=((ct=o[0])==null?void 0:ct.isAuth)&&Tt(o),de=(ft=o[0])==null?void 0:ft.schema;const tt=t=>t[12].name;for(let t=0;t<de.length;t+=1){let i=St(o,de,t),y=tt(i);Ne.set(y,V[t]=qt(y,i))}te=new At({props:{content:"?expand=relField1,relField2.subRelField"}});let ce=o[2];const lt=t=>t[7].code;for(let t=0;t<ce.length;t+=1){let i=Ct(o,ce,t),y=lt(i);Ze.set(y,J[t]=Mt(y,i))}let re=o[2];const nt=t=>t[7].code;for(let t=0;t<re.length;t+=1){let i=wt(o,re,t),y=nt(i);et.set(y,B[t]=Ot(y,i))}return{c(){e=a("h3"),l=k("Create ("),b=k(s),p=k(")"),c=m(),f=a("div"),v=a("p"),T=k("Create a new "),w=a("strong"),g=k(M),D=k(" record."),E=m(),L=a("p"),L.innerHTML=`Body parameters could be sent as <code>application/json</code> or
|
||||
<code>multipart/form-data</code>.`,I=m(),j=a("p"),j.innerHTML=`File upload is supported only via <code>multipart/form-data</code>.
|
||||
<br/>
|
||||
For more info and examples you could check the detailed
|
||||
<a href="https://pocketbase.io/docs/files-handling/" target="_blank" rel="noopener noreferrer">Files upload and handling docs
|
||||
</a>.`,R=m(),Pe(S.$$.fragment),N=m(),q=a("h6"),q.textContent="API details",C=m(),_=a("div"),O=a("strong"),O.textContent="POST",K=m(),P=a("div"),u=a("p"),$=k("/api/collections/"),ee=a("strong"),me=k(Q),Re=k("/records"),ge=m(),F&&F.c(),be=m(),ne=a("div"),ne.textContent="Body Parameters",_e=m(),W=a("table"),ke=a("thead"),ke.innerHTML=`<tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th></tr>`,je=m(),U=a("tbody"),ye=a("tr"),ye.innerHTML=`<td><div class="inline-flex"><span class="label label-warning">Optional</span>
|
||||
<span>id</span></div></td>
|
||||
<td><span class="label">String</span></td>
|
||||
<td><strong>15 characters string</strong> to store as record ID.
|
||||
<br/>
|
||||
If not set, it will be auto generated.</td>`,De=m(),H&&H.c(),ve=m();for(let t=0;t<V.length;t+=1)V[t].c();he=m(),se=a("div"),se.textContent="Query parameters",we=m(),Y=a("table"),Ce=a("thead"),Ce.innerHTML=`<tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th></tr>`,Ve=m(),Se=a("tbody"),G=a("tr"),$e=a("td"),$e.textContent="expand",Je=m(),Te=a("td"),Te.innerHTML='<span class="label">String</span>',Ee=m(),A=a("td"),Ie=k(`Auto expand relations when returning the created record. Ex.:
|
||||
`),Pe(te.$$.fragment),Ue=k(`
|
||||
Supports up to 6-levels depth nested relations expansion. `),ze=a("br"),Ke=k(`
|
||||
The expanded relations will be appended to the record under the
|
||||
`),qe=a("code"),qe.textContent="expand",Qe=k(" property (eg. "),Me=a("code"),Me.textContent='"expand": {"relField1": {...}, ...}',We=k(`).
|
||||
`),Ye=a("br"),Ge=k(`
|
||||
Only the relations to which the request user has permissions to `),Oe=a("strong"),Oe.textContent="view",Xe=k(" will be expanded."),Ae=m(),ie=a("div"),ie.textContent="Responses",He=m(),X=a("div"),ae=a("div");for(let t=0;t<J.length;t+=1)J[t].c();xe=m(),oe=a("div");for(let t=0;t<B.length;t+=1)B[t].c();h(e,"class","m-b-sm"),h(f,"class","content txt-lg m-b-sm"),h(q,"class","m-b-xs"),h(O,"class","label label-primary"),h(P,"class","content"),h(_,"class","alert alert-success"),h(ne,"class","section-title"),h(W,"class","table-compact table-border m-b-base"),h(se,"class","section-title"),h(Y,"class","table-compact table-border m-b-base"),h(ie,"class","section-title"),h(ae,"class","tabs-header compact left"),h(oe,"class","tabs-content"),h(X,"class","tabs")},m(t,i){r(t,e,i),n(e,l),n(e,b),n(e,p),r(t,c,i),r(t,f,i),n(f,v),n(v,T),n(v,w),n(w,g),n(v,D),n(f,E),n(f,L),n(f,I),n(f,j),r(t,R,i),Be(S,t,i),r(t,N,i),r(t,q,i),r(t,C,i),r(t,_,i),n(_,O),n(_,K),n(_,P),n(P,u),n(u,$),n(u,ee),n(ee,me),n(u,Re),n(_,ge),F&&F.m(_,null),r(t,be,i),r(t,ne,i),r(t,_e,i),r(t,W,i),n(W,ke),n(W,je),n(W,U),n(U,ye),n(U,De),H&&H.m(U,null),n(U,ve);for(let y=0;y<V.length;y+=1)V[y]&&V[y].m(U,null);r(t,he,i),r(t,se,i),r(t,we,i),r(t,Y,i),n(Y,Ce),n(Y,Ve),n(Y,Se),n(Se,G),n(G,$e),n(G,Je),n(G,Te),n(G,Ee),n(G,A),n(A,Ie),Be(te,A,null),n(A,Ue),n(A,ze),n(A,Ke),n(A,qe),n(A,Qe),n(A,Me),n(A,We),n(A,Ye),n(A,Ge),n(A,Oe),n(A,Xe),r(t,Ae,i),r(t,ie,i),r(t,He,i),r(t,X,i),n(X,ae);for(let y=0;y<J.length;y+=1)J[y]&&J[y].m(ae,null);n(X,xe),n(X,oe);for(let y=0;y<B.length;y+=1)B[y]&&B[y].m(oe,null);Z=!0},p(t,[i]){var pt,ut,mt,bt,_t,kt,yt,vt;(!Z||i&1)&&s!==(s=t[0].name+"")&&x(b,s),(!Z||i&1)&&M!==(M=t[0].name+"")&&x(g,M);const y={};i&25&&(y.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${t[4]}');
|
||||
|
||||
...
|
||||
|
||||
// example create data
|
||||
const data = ${JSON.stringify(Object.assign({},t[3],z.dummyCollectionSchemaData(t[0])),null,4)};
|
||||
|
||||
const record = await pb.collection('${(pt=t[0])==null?void 0:pt.name}').create(data);
|
||||
`+((ut=t[0])!=null&&ut.isAuth?`
|
||||
// (optional) send an email verification request
|
||||
await pb.collection('${(mt=t[0])==null?void 0:mt.name}').requestVerification('test@example.com');
|
||||
`:"")),i&25&&(y.dart=`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${t[4]}');
|
||||
|
||||
...
|
||||
|
||||
// example create body
|
||||
final body = <String, dynamic>${JSON.stringify(Object.assign({},t[3],z.dummyCollectionSchemaData(t[0])),null,2)};
|
||||
|
||||
final record = await pb.collection('${(bt=t[0])==null?void 0:bt.name}').create(body: body);
|
||||
`+((_t=t[0])!=null&&_t.isAuth?`
|
||||
// (optional) send an email verification request
|
||||
await pb.collection('${(kt=t[0])==null?void 0:kt.name}').requestVerification('test@example.com');
|
||||
`:"")),S.$set(y),(!Z||i&1)&&Q!==(Q=t[0].name+"")&&x(me,Q),t[5]?F||(F=$t(),F.c(),F.m(_,null)):F&&(F.d(1),F=null),(yt=t[0])!=null&&yt.isAuth?H?H.p(t,i):(H=Tt(t),H.c(),H.m(U,ve)):H&&(H.d(1),H=null),i&1&&(de=(vt=t[0])==null?void 0:vt.schema,V=Le(V,i,tt,1,t,de,Ne,U,ht,qt,null,St)),i&6&&(ce=t[2],J=Le(J,i,lt,1,t,ce,Ze,ae,ht,Mt,null,Ct)),i&6&&(re=t[2],Bt(),B=Le(B,i,nt,1,t,re,et,oe,Ft,Ot,null,wt),Rt())},i(t){if(!Z){fe(S.$$.fragment,t),fe(te.$$.fragment,t);for(let i=0;i<re.length;i+=1)fe(B[i]);Z=!0}},o(t){pe(S.$$.fragment,t),pe(te.$$.fragment,t);for(let i=0;i<B.length;i+=1)pe(B[i]);Z=!1},d(t){t&&d(e),t&&d(c),t&&d(f),t&&d(R),Fe(S,t),t&&d(N),t&&d(q),t&&d(C),t&&d(_),F&&F.d(),t&&d(be),t&&d(ne),t&&d(_e),t&&d(W),H&&H.d();for(let i=0;i<V.length;i+=1)V[i].d();t&&d(he),t&&d(se),t&&d(we),t&&d(Y),Fe(te),t&&d(Ae),t&&d(ie),t&&d(He),t&&d(X);for(let i=0;i<J.length;i+=1)J[i].d();for(let i=0;i<B.length;i+=1)B[i].d()}}}function Zt(o,e,l){let s,b,{collection:p=new gt}=e,c=200,f=[],v={};const T=w=>l(1,c=w.code);return o.$$set=w=>{"collection"in w&&l(0,p=w.collection)},o.$$.update=()=>{var w,M;o.$$.dirty&1&&l(5,s=(p==null?void 0:p.createRule)===null),o.$$.dirty&1&&l(2,f=[{code:200,body:JSON.stringify(z.dummyCollectionRecord(p),null,2)},{code:400,body:`
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to create record.",
|
||||
"data": {
|
||||
"${(M=(w=p==null?void 0:p.schema)==null?void 0:w[0])==null?void 0:M.name}": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`},{code:403,body:`
|
||||
{
|
||||
"code": 403,
|
||||
"message": "You are not allowed to perform this request.",
|
||||
"data": {}
|
||||
}
|
||||
`}]),o.$$.dirty&1&&(p.$isAuth?l(3,v={username:"test_username",email:"test@example.com",emailVisibility:!0,password:"12345678",passwordConfirm:"12345678"}):l(3,v={}))},l(4,b=z.getApiExampleUrl(jt.baseUrl)),[p,c,f,v,b,s,T]}class tl extends Ht{constructor(e){super(),Lt(this,e,Zt,Xt,Pt,{collection:0})}}export{tl as default};
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import{S as Ce,i as Re,s as Pe,e as c,w as $,b as h,c as $e,f as m,g as f,h as n,m as we,x,N as _e,O as Ee,k as Te,P as Oe,n as Be,t as ee,a as te,o as u,d as ge,T as Ie,C as Me,p as Ae,r as N,u as Se,M as qe}from"./index-79bb649c.js";import{S as He}from"./SdkTabs-22bd33d6.js";function ke(o,l,s){const a=o.slice();return a[6]=l[s],a}function he(o,l,s){const a=o.slice();return a[6]=l[s],a}function ve(o){let l;return{c(){l=c("p"),l.innerHTML="Requires admin <code>Authorization:TOKEN</code> header",m(l,"class","txt-hint txt-sm txt-right")},m(s,a){f(s,l,a)},d(s){s&&u(l)}}}function ye(o,l){let s,a=l[6].code+"",v,i,r,p;function w(){return l[5](l[6])}return{key:o,first:null,c(){s=c("button"),v=$(a),i=h(),m(s,"class","tab-item"),N(s,"active",l[2]===l[6].code),this.first=s},m(b,g){f(b,s,g),n(s,v),n(s,i),r||(p=Se(s,"click",w),r=!0)},p(b,g){l=b,g&20&&N(s,"active",l[2]===l[6].code)},d(b){b&&u(s),r=!1,p()}}}function De(o,l){let s,a,v,i;return a=new qe({props:{content:l[6].body}}),{key:o,first:null,c(){s=c("div"),$e(a.$$.fragment),v=h(),m(s,"class","tab-item"),N(s,"active",l[2]===l[6].code),this.first=s},m(r,p){f(r,s,p),we(a,s,null),n(s,v),i=!0},p(r,p){l=r,(!i||p&20)&&N(s,"active",l[2]===l[6].code)},i(r){i||(ee(a.$$.fragment,r),i=!0)},o(r){te(a.$$.fragment,r),i=!1},d(r){r&&u(s),ge(a)}}}function Le(o){var ue,pe;let l,s,a=o[0].name+"",v,i,r,p,w,b,g,q=o[0].name+"",z,le,F,C,K,T,G,y,H,se,L,E,oe,J,U=o[0].name+"",Q,ae,V,ne,W,O,X,B,Y,I,Z,R,M,D=[],ie=new Map,re,A,_=[],ce=new Map,P;C=new He({props:{js:`
|
||||
import{S as Ce,i as Re,s as Pe,e as c,w as $,b as h,c as $e,f as m,g as f,h as n,m as we,x,N as _e,P as Ee,k as Te,Q as Be,n as Oe,t as ee,a as te,o as u,d as ge,T as Ie,C as Me,p as Ae,r as N,u as Se,M as qe}from"./index-6e04d404.js";import{S as He}from"./SdkTabs-1a82fff4.js";function ke(o,l,s){const a=o.slice();return a[6]=l[s],a}function he(o,l,s){const a=o.slice();return a[6]=l[s],a}function ve(o){let l;return{c(){l=c("p"),l.innerHTML="Requires admin <code>Authorization:TOKEN</code> header",m(l,"class","txt-hint txt-sm txt-right")},m(s,a){f(s,l,a)},d(s){s&&u(l)}}}function ye(o,l){let s,a=l[6].code+"",v,i,r,p;function w(){return l[5](l[6])}return{key:o,first:null,c(){s=c("button"),v=$(a),i=h(),m(s,"class","tab-item"),N(s,"active",l[2]===l[6].code),this.first=s},m(b,g){f(b,s,g),n(s,v),n(s,i),r||(p=Se(s,"click",w),r=!0)},p(b,g){l=b,g&20&&N(s,"active",l[2]===l[6].code)},d(b){b&&u(s),r=!1,p()}}}function De(o,l){let s,a,v,i;return a=new qe({props:{content:l[6].body}}),{key:o,first:null,c(){s=c("div"),$e(a.$$.fragment),v=h(),m(s,"class","tab-item"),N(s,"active",l[2]===l[6].code),this.first=s},m(r,p){f(r,s,p),we(a,s,null),n(s,v),i=!0},p(r,p){l=r,(!i||p&20)&&N(s,"active",l[2]===l[6].code)},i(r){i||(ee(a.$$.fragment,r),i=!0)},o(r){te(a.$$.fragment,r),i=!1},d(r){r&&u(s),ge(a)}}}function Le(o){var ue,pe;let l,s,a=o[0].name+"",v,i,r,p,w,b,g,q=o[0].name+"",z,le,F,C,K,T,Q,y,H,se,L,E,oe,G,U=o[0].name+"",J,ae,V,ne,W,B,X,O,Y,I,Z,R,M,D=[],ie=new Map,re,A,_=[],ce=new Map,P;C=new He({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${o[3]}');
|
||||
@@ -14,12 +14,12 @@ import{S as Ce,i as Re,s as Pe,e as c,w as $,b as h,c as $e,f as m,g as f,h as n
|
||||
...
|
||||
|
||||
await pb.collection('${(pe=o[0])==null?void 0:pe.name}').delete('RECORD_ID');
|
||||
`}});let k=o[1]&&ve(),j=o[4];const de=e=>e[6].code;for(let e=0;e<j.length;e+=1){let t=he(o,j,e),d=de(t);ie.set(d,D[e]=ye(d,t))}let S=o[4];const fe=e=>e[6].code;for(let e=0;e<S.length;e+=1){let t=ke(o,S,e),d=fe(t);ce.set(d,_[e]=De(d,t))}return{c(){l=c("h3"),s=$("Delete ("),v=$(a),i=$(")"),r=h(),p=c("div"),w=c("p"),b=$("Delete a single "),g=c("strong"),z=$(q),le=$(" record."),F=h(),$e(C.$$.fragment),K=h(),T=c("h6"),T.textContent="API details",G=h(),y=c("div"),H=c("strong"),H.textContent="DELETE",se=h(),L=c("div"),E=c("p"),oe=$("/api/collections/"),J=c("strong"),Q=$(U),ae=$("/records/"),V=c("strong"),V.textContent=":id",ne=h(),k&&k.c(),W=h(),O=c("div"),O.textContent="Path parameters",X=h(),B=c("table"),B.innerHTML=`<thead><tr><th>Param</th>
|
||||
`}});let k=o[1]&&ve(),j=o[4];const de=e=>e[6].code;for(let e=0;e<j.length;e+=1){let t=he(o,j,e),d=de(t);ie.set(d,D[e]=ye(d,t))}let S=o[4];const fe=e=>e[6].code;for(let e=0;e<S.length;e+=1){let t=ke(o,S,e),d=fe(t);ce.set(d,_[e]=De(d,t))}return{c(){l=c("h3"),s=$("Delete ("),v=$(a),i=$(")"),r=h(),p=c("div"),w=c("p"),b=$("Delete a single "),g=c("strong"),z=$(q),le=$(" record."),F=h(),$e(C.$$.fragment),K=h(),T=c("h6"),T.textContent="API details",Q=h(),y=c("div"),H=c("strong"),H.textContent="DELETE",se=h(),L=c("div"),E=c("p"),oe=$("/api/collections/"),G=c("strong"),J=$(U),ae=$("/records/"),V=c("strong"),V.textContent=":id",ne=h(),k&&k.c(),W=h(),B=c("div"),B.textContent="Path parameters",X=h(),O=c("table"),O.innerHTML=`<thead><tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th></tr></thead>
|
||||
<tbody><tr><td>id</td>
|
||||
<td><span class="label">String</span></td>
|
||||
<td>ID of the record to delete.</td></tr></tbody>`,Y=h(),I=c("div"),I.textContent="Responses",Z=h(),R=c("div"),M=c("div");for(let e=0;e<D.length;e+=1)D[e].c();re=h(),A=c("div");for(let e=0;e<_.length;e+=1)_[e].c();m(l,"class","m-b-sm"),m(p,"class","content txt-lg m-b-sm"),m(T,"class","m-b-xs"),m(H,"class","label label-primary"),m(L,"class","content"),m(y,"class","alert alert-danger"),m(O,"class","section-title"),m(B,"class","table-compact table-border m-b-base"),m(I,"class","section-title"),m(M,"class","tabs-header compact left"),m(A,"class","tabs-content"),m(R,"class","tabs")},m(e,t){f(e,l,t),n(l,s),n(l,v),n(l,i),f(e,r,t),f(e,p,t),n(p,w),n(w,b),n(w,g),n(g,z),n(w,le),f(e,F,t),we(C,e,t),f(e,K,t),f(e,T,t),f(e,G,t),f(e,y,t),n(y,H),n(y,se),n(y,L),n(L,E),n(E,oe),n(E,J),n(J,Q),n(E,ae),n(E,V),n(y,ne),k&&k.m(y,null),f(e,W,t),f(e,O,t),f(e,X,t),f(e,B,t),f(e,Y,t),f(e,I,t),f(e,Z,t),f(e,R,t),n(R,M);for(let d=0;d<D.length;d+=1)D[d]&&D[d].m(M,null);n(R,re),n(R,A);for(let d=0;d<_.length;d+=1)_[d]&&_[d].m(A,null);P=!0},p(e,[t]){var me,be;(!P||t&1)&&a!==(a=e[0].name+"")&&x(v,a),(!P||t&1)&&q!==(q=e[0].name+"")&&x(z,q);const d={};t&9&&(d.js=`
|
||||
<td>ID of the record to delete.</td></tr></tbody>`,Y=h(),I=c("div"),I.textContent="Responses",Z=h(),R=c("div"),M=c("div");for(let e=0;e<D.length;e+=1)D[e].c();re=h(),A=c("div");for(let e=0;e<_.length;e+=1)_[e].c();m(l,"class","m-b-sm"),m(p,"class","content txt-lg m-b-sm"),m(T,"class","m-b-xs"),m(H,"class","label label-primary"),m(L,"class","content"),m(y,"class","alert alert-danger"),m(B,"class","section-title"),m(O,"class","table-compact table-border m-b-base"),m(I,"class","section-title"),m(M,"class","tabs-header compact left"),m(A,"class","tabs-content"),m(R,"class","tabs")},m(e,t){f(e,l,t),n(l,s),n(l,v),n(l,i),f(e,r,t),f(e,p,t),n(p,w),n(w,b),n(w,g),n(g,z),n(w,le),f(e,F,t),we(C,e,t),f(e,K,t),f(e,T,t),f(e,Q,t),f(e,y,t),n(y,H),n(y,se),n(y,L),n(L,E),n(E,oe),n(E,G),n(G,J),n(E,ae),n(E,V),n(y,ne),k&&k.m(y,null),f(e,W,t),f(e,B,t),f(e,X,t),f(e,O,t),f(e,Y,t),f(e,I,t),f(e,Z,t),f(e,R,t),n(R,M);for(let d=0;d<D.length;d+=1)D[d]&&D[d].m(M,null);n(R,re),n(R,A);for(let d=0;d<_.length;d+=1)_[d]&&_[d].m(A,null);P=!0},p(e,[t]){var me,be;(!P||t&1)&&a!==(a=e[0].name+"")&&x(v,a),(!P||t&1)&&q!==(q=e[0].name+"")&&x(z,q);const d={};t&9&&(d.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${e[3]}');
|
||||
@@ -35,7 +35,7 @@ import{S as Ce,i as Re,s as Pe,e as c,w as $,b as h,c as $e,f as m,g as f,h as n
|
||||
...
|
||||
|
||||
await pb.collection('${(be=e[0])==null?void 0:be.name}').delete('RECORD_ID');
|
||||
`),C.$set(d),(!P||t&1)&&U!==(U=e[0].name+"")&&x(Q,U),e[1]?k||(k=ve(),k.c(),k.m(y,null)):k&&(k.d(1),k=null),t&20&&(j=e[4],D=_e(D,t,de,1,e,j,ie,M,Ee,ye,null,he)),t&20&&(S=e[4],Te(),_=_e(_,t,fe,1,e,S,ce,A,Oe,De,null,ke),Be())},i(e){if(!P){ee(C.$$.fragment,e);for(let t=0;t<S.length;t+=1)ee(_[t]);P=!0}},o(e){te(C.$$.fragment,e);for(let t=0;t<_.length;t+=1)te(_[t]);P=!1},d(e){e&&u(l),e&&u(r),e&&u(p),e&&u(F),ge(C,e),e&&u(K),e&&u(T),e&&u(G),e&&u(y),k&&k.d(),e&&u(W),e&&u(O),e&&u(X),e&&u(B),e&&u(Y),e&&u(I),e&&u(Z),e&&u(R);for(let t=0;t<D.length;t+=1)D[t].d();for(let t=0;t<_.length;t+=1)_[t].d()}}}function Ue(o,l,s){let a,v,{collection:i=new Ie}=l,r=204,p=[];const w=b=>s(2,r=b.code);return o.$$set=b=>{"collection"in b&&s(0,i=b.collection)},o.$$.update=()=>{o.$$.dirty&1&&s(1,a=(i==null?void 0:i.deleteRule)===null),o.$$.dirty&3&&i!=null&&i.id&&(p.push({code:204,body:`
|
||||
`),C.$set(d),(!P||t&1)&&U!==(U=e[0].name+"")&&x(J,U),e[1]?k||(k=ve(),k.c(),k.m(y,null)):k&&(k.d(1),k=null),t&20&&(j=e[4],D=_e(D,t,de,1,e,j,ie,M,Ee,ye,null,he)),t&20&&(S=e[4],Te(),_=_e(_,t,fe,1,e,S,ce,A,Be,De,null,ke),Oe())},i(e){if(!P){ee(C.$$.fragment,e);for(let t=0;t<S.length;t+=1)ee(_[t]);P=!0}},o(e){te(C.$$.fragment,e);for(let t=0;t<_.length;t+=1)te(_[t]);P=!1},d(e){e&&u(l),e&&u(r),e&&u(p),e&&u(F),ge(C,e),e&&u(K),e&&u(T),e&&u(Q),e&&u(y),k&&k.d(),e&&u(W),e&&u(B),e&&u(X),e&&u(O),e&&u(Y),e&&u(I),e&&u(Z),e&&u(R);for(let t=0;t<D.length;t+=1)D[t].d();for(let t=0;t<_.length;t+=1)_[t].d()}}}function Ue(o,l,s){let a,v,{collection:i=new Ie}=l,r=204,p=[];const w=b=>s(2,r=b.code);return o.$$set=b=>{"collection"in b&&s(0,i=b.collection)},o.$$.update=()=>{o.$$.dirty&1&&s(1,a=(i==null?void 0:i.deleteRule)===null),o.$$.dirty&3&&i!=null&&i.id&&(p.push({code:204,body:`
|
||||
null
|
||||
`}),p.push({code:400,body:`
|
||||
{
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import{S as d,i as n,s as i,e as l,g as o,y as s,o as p}from"./index-6e04d404.js";function c(a){let e;return{c(){e=l("tr"),e.innerHTML=`<td id="query-page">fields</td>
|
||||
<td><span class="label">String</span></td>
|
||||
<td>Comma separated string of the fields to return in the JSON response
|
||||
<em>(by default returns all fields)</em>. For example:
|
||||
<br/>
|
||||
<code>?fields=id,expand.relField.id,expand.relField.created</code></td>`},m(t,r){o(t,e,r)},p:s,i:s,o:s,d(t){t&&p(e)}}}class f extends d{constructor(e){super(),n(this,e,null,c,i,{})}}export{f as F};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{S as oe,i as ae,s as le,e as ue,f as ce,g as fe,y as H,o as de,I as he,J as ge,K as pe,H as ye,C as q,L as me}from"./index-79bb649c.js";import{E as S,a as w,h as be,b as ke,c as xe,d as Ke,e as Ce,s as qe,f as Se,g as we,r as Le,i as Ie,k as Ee,j as Re,l as Ae,m as Be,n as Oe,o as _e,p as ve,q as Y,C as E,S as He,t as De}from"./index-8390b00e.js";function Me(e){Z(e,"start");var i={},n=e.languageData||{},g=!1;for(var p in e)if(p!=n&&e.hasOwnProperty(p))for(var d=i[p]=[],o=e[p],s=0;s<o.length;s++){var l=o[s];d.push(new Ue(l,e)),(l.indent||l.dedent)&&(g=!0)}return{name:n.name,startState:function(){return{state:"start",pending:null,indent:g?[]:null}},copyState:function(a){var b={state:a.state,pending:a.pending,indent:a.indent&&a.indent.slice(0)};return a.stack&&(b.stack=a.stack.slice(0)),b},token:We(i),indent:Ne(i,n),languageData:n}}function Z(e,i){if(!e.hasOwnProperty(i))throw new Error("Undefined state "+i+" in simple mode")}function Fe(e,i){if(!e)return/(?:)/;var n="";return e instanceof RegExp?(e.ignoreCase&&(n="i"),e=e.source):e=String(e),new RegExp((i===!1?"":"^")+"(?:"+e+")",n)}function Te(e){if(!e)return null;if(e.apply)return e;if(typeof e=="string")return e.replace(/\./g," ");for(var i=[],n=0;n<e.length;n++)i.push(e[n]&&e[n].replace(/\./g," "));return i}function Ue(e,i){(e.next||e.push)&&Z(i,e.next||e.push),this.regex=Fe(e.regex),this.token=Te(e.token),this.data=e}function We(e){return function(i,n){if(n.pending){var g=n.pending.shift();return n.pending.length==0&&(n.pending=null),i.pos+=g.text.length,g.token}for(var p=e[n.state],d=0;d<p.length;d++){var o=p[d],s=(!o.data.sol||i.sol())&&i.match(o.regex);if(s){o.data.next?n.state=o.data.next:o.data.push?((n.stack||(n.stack=[])).push(n.state),n.state=o.data.push):o.data.pop&&n.stack&&n.stack.length&&(n.state=n.stack.pop()),o.data.indent&&n.indent.push(i.indentation()+i.indentUnit),o.data.dedent&&n.indent.pop();var l=o.token;if(l&&l.apply&&(l=l(s)),s.length>2&&o.token&&typeof o.token!="string"){n.pending=[];for(var a=2;a<s.length;a++)s[a]&&n.pending.push({text:s[a],token:o.token[a-1]});return i.backUp(s[0].length-(s[1]?s[1].length:0)),l[0]}else return l&&l.join?l[0]:l}}return i.next(),null}}function Ne(e,i){return function(n,g){if(n.indent==null||i.dontIndentStates&&i.doneIndentState.indexOf(n.state)>-1)return null;var p=n.indent.length-1,d=e[n.state];e:for(;;){for(var o=0;o<d.length;o++){var s=d[o];if(s.data.dedent&&s.data.dedentIfLineStart!==!1){var l=s.regex.exec(g);if(l&&l[0]){p--,(s.next||s.push)&&(d=e[s.next||s.push]),g=g.slice(l[0].length);continue e}}}break}return p<0?0:n.indent[p]}}function Je(e){let i;return{c(){i=ue("div"),ce(i,"class","code-editor")},m(n,g){fe(n,i,g),e[15](i)},p:H,i:H,o:H,d(n){n&&de(i),e[15](null)}}}function Pe(e){return JSON.stringify([e==null?void 0:e.name,e==null?void 0:e.type,e==null?void 0:e.schema])}function Ve(e,i,n){let g;he(e,ge,t=>n(21,g=t));const p=pe();let{id:d=""}=i,{value:o=""}=i,{disabled:s=!1}=i,{placeholder:l=""}=i,{baseCollection:a=null}=i,{singleLine:b=!1}=i,{extraAutocompleteKeys:R=[]}=i,{disableRequestKeys:x=!1}=i,{disableIndirectCollectionsKeys:K=!1}=i,f,k,A=s,D=new E,M=new E,F=new E,T=new E,L=[],U=[],W=[],N=[],I="",B="";function O(){f==null||f.focus()}let _=null;function j(){clearTimeout(_),_=setTimeout(()=>{L=$(g),N=ee(),U=x?[]:te(),W=K?[]:ne()},300)}function $(t){let r=t.slice();return a&&q.pushOrReplaceByKey(r,a,"id"),r}function J(){k==null||k.dispatchEvent(new CustomEvent("change",{detail:{value:o},bubbles:!0}))}function P(){if(!d)return;const t=document.querySelectorAll('[for="'+d+'"]');for(let r of t)r.removeEventListener("click",O)}function V(){if(!d)return;P();const t=document.querySelectorAll('[for="'+d+'"]');for(let r of t)r.addEventListener("click",O)}function C(t,r="",c=0){var m,z,Q;let h=L.find(y=>y.name==t||y.id==t);if(!h||c>=4)return[];let u=q.getAllCollectionIdentifiers(h,r);for(const y of h.schema){const v=r+y.name;if(y.type==="relation"&&((m=y.options)!=null&&m.collectionId)){const X=C(y.options.collectionId,v+".",c+1);X.length&&(u=u.concat(X))}y.type==="select"&&((z=y.options)==null?void 0:z.maxSelect)!=1&&u.push(v+":each"),((Q=y.options)==null?void 0:Q.maxSelect)!=1&&["select","file","relation"].includes(y.type)&&u.push(v+":length")}return u}function ee(){return C(a==null?void 0:a.name)}function te(){const t=[];t.push("@request.method"),t.push("@request.query."),t.push("@request.data."),t.push("@request.headers."),t.push("@request.auth.id"),t.push("@request.auth.collectionId"),t.push("@request.auth.collectionName"),t.push("@request.auth.verified"),t.push("@request.auth.username"),t.push("@request.auth.email"),t.push("@request.auth.emailVisibility"),t.push("@request.auth.created"),t.push("@request.auth.updated");const r=L.filter(h=>h.$isAuth);for(const h of r){const u=C(h.id,"@request.auth.");for(const m of u)q.pushUnique(t,m)}const c=["created","updated"];if(a!=null&&a.id){const h=C(a.name,"@request.data.");for(const u of h){t.push(u);const m=u.split(".");m.length===3&&m[2].inLine truncated
|
||||
import{S as oe,i as ae,s as le,e as ue,f as ce,g as fe,y as H,o as de,I as he,J as ge,K as pe,H as ye,C as q,L as me}from"./index-6e04d404.js";import{E as S,a as w,h as be,b as ke,c as xe,d as Ke,e as Ce,s as qe,f as Se,g as we,r as Le,i as Ie,k as Ee,j as Re,l as Ae,m as Be,n as Oe,o as _e,p as ve,q as Y,C as E,S as He,t as De}from"./index-91aae1cf.js";function Me(e){Z(e,"start");var i={},n=e.languageData||{},g=!1;for(var p in e)if(p!=n&&e.hasOwnProperty(p))for(var d=i[p]=[],o=e[p],s=0;s<o.length;s++){var l=o[s];d.push(new Ue(l,e)),(l.indent||l.dedent)&&(g=!0)}return{name:n.name,startState:function(){return{state:"start",pending:null,indent:g?[]:null}},copyState:function(a){var b={state:a.state,pending:a.pending,indent:a.indent&&a.indent.slice(0)};return a.stack&&(b.stack=a.stack.slice(0)),b},token:We(i),indent:Ne(i,n),languageData:n}}function Z(e,i){if(!e.hasOwnProperty(i))throw new Error("Undefined state "+i+" in simple mode")}function Fe(e,i){if(!e)return/(?:)/;var n="";return e instanceof RegExp?(e.ignoreCase&&(n="i"),e=e.source):e=String(e),new RegExp((i===!1?"":"^")+"(?:"+e+")",n)}function Te(e){if(!e)return null;if(e.apply)return e;if(typeof e=="string")return e.replace(/\./g," ");for(var i=[],n=0;n<e.length;n++)i.push(e[n]&&e[n].replace(/\./g," "));return i}function Ue(e,i){(e.next||e.push)&&Z(i,e.next||e.push),this.regex=Fe(e.regex),this.token=Te(e.token),this.data=e}function We(e){return function(i,n){if(n.pending){var g=n.pending.shift();return n.pending.length==0&&(n.pending=null),i.pos+=g.text.length,g.token}for(var p=e[n.state],d=0;d<p.length;d++){var o=p[d],s=(!o.data.sol||i.sol())&&i.match(o.regex);if(s){o.data.next?n.state=o.data.next:o.data.push?((n.stack||(n.stack=[])).push(n.state),n.state=o.data.push):o.data.pop&&n.stack&&n.stack.length&&(n.state=n.stack.pop()),o.data.indent&&n.indent.push(i.indentation()+i.indentUnit),o.data.dedent&&n.indent.pop();var l=o.token;if(l&&l.apply&&(l=l(s)),s.length>2&&o.token&&typeof o.token!="string"){n.pending=[];for(var a=2;a<s.length;a++)s[a]&&n.pending.push({text:s[a],token:o.token[a-1]});return i.backUp(s[0].length-(s[1]?s[1].length:0)),l[0]}else return l&&l.join?l[0]:l}}return i.next(),null}}function Ne(e,i){return function(n,g){if(n.indent==null||i.dontIndentStates&&i.doneIndentState.indexOf(n.state)>-1)return null;var p=n.indent.length-1,d=e[n.state];e:for(;;){for(var o=0;o<d.length;o++){var s=d[o];if(s.data.dedent&&s.data.dedentIfLineStart!==!1){var l=s.regex.exec(g);if(l&&l[0]){p--,(s.next||s.push)&&(d=e[s.next||s.push]),g=g.slice(l[0].length);continue e}}}break}return p<0?0:n.indent[p]}}function Je(e){let i;return{c(){i=ue("div"),ce(i,"class","code-editor")},m(n,g){fe(n,i,g),e[15](i)},p:H,i:H,o:H,d(n){n&&de(i),e[15](null)}}}function Pe(e){return JSON.stringify([e==null?void 0:e.name,e==null?void 0:e.type,e==null?void 0:e.schema])}function Ve(e,i,n){let g;he(e,ge,t=>n(21,g=t));const p=pe();let{id:d=""}=i,{value:o=""}=i,{disabled:s=!1}=i,{placeholder:l=""}=i,{baseCollection:a=null}=i,{singleLine:b=!1}=i,{extraAutocompleteKeys:R=[]}=i,{disableRequestKeys:x=!1}=i,{disableIndirectCollectionsKeys:K=!1}=i,f,k,A=s,D=new E,M=new E,F=new E,T=new E,L=[],U=[],W=[],N=[],I="",B="";function O(){f==null||f.focus()}let _=null;function j(){clearTimeout(_),_=setTimeout(()=>{L=$(g),N=ee(),U=x?[]:te(),W=K?[]:ne()},300)}function $(t){let r=t.slice();return a&&q.pushOrReplaceByKey(r,a,"id"),r}function J(){k==null||k.dispatchEvent(new CustomEvent("change",{detail:{value:o},bubbles:!0}))}function P(){if(!d)return;const t=document.querySelectorAll('[for="'+d+'"]');for(let r of t)r.removeEventListener("click",O)}function V(){if(!d)return;P();const t=document.querySelectorAll('[for="'+d+'"]');for(let r of t)r.addEventListener("click",O)}function C(t,r="",c=0){var m,z,Q;let h=L.find(y=>y.name==t||y.id==t);if(!h||c>=4)return[];let u=q.getAllCollectionIdentifiers(h,r);for(const y of h.schema){const v=r+y.name;if(y.type==="relation"&&((m=y.options)!=null&&m.collectionId)){const X=C(y.options.collectionId,v+".",c+1);X.length&&(u=u.concat(X))}y.type==="select"&&((z=y.options)==null?void 0:z.maxSelect)!=1&&u.push(v+":each"),((Q=y.options)==null?void 0:Q.maxSelect)!=1&&["select","file","relation"].includes(y.type)&&u.push(v+":length")}return u}function ee(){return C(a==null?void 0:a.name)}function te(){const t=[];t.push("@request.method"),t.push("@request.query."),t.push("@request.data."),t.push("@request.headers."),t.push("@request.auth.id"),t.push("@request.auth.collectionId"),t.push("@request.auth.collectionName"),t.push("@request.auth.verified"),t.push("@request.auth.username"),t.push("@request.auth.email"),t.push("@request.auth.emailVisibility"),t.push("@request.auth.created"),t.push("@request.auth.updated");const r=L.filter(h=>h.$isAuth);for(const h of r){const u=C(h.id,"@request.auth.");for(const m of u)q.pushUnique(t,m)}const c=["created","updated"];if(a!=null&&a.id){const h=C(a.name,"@request.data.");for(const u of h){t.push(u);const m=u.split(".");m.length===3&&m[2].inLine truncated
|
||||
Loaded 100 of 209 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user