Compare commits

...
116 Commits
Author SHA1 Message Date
Gani Georgiev b9993aaa73 updated changelog 2023-07-14 12:16:56 +03:00
Gani Georgiev f77fb0cc1c updated tests with some clarification code comments 2023-07-14 12:13:44 +03:00
Gani Georgiev 460cc35bb6 updated ui/dist 2023-07-14 12:01:48 +03:00
Gani Georgiev f0bcffec8b [#2914] register the eagerRequestDataCache middleware only for the api grroup to avoid conflicts with custom routes 2023-07-14 11:55:29 +03:00
Gani Georgiev c3844250e8 updated go deps 2023-07-08 20:06:34 +03:00
Gani Georgiev ebfbb55f91 allow no space between the index table name and columns list 2023-07-06 23:20:31 +03:00
Gani Georgiev d77479131a [#2868] fixed unique validator detailed error message not being returned when camelCase field name is used 2023-07-06 23:14:18 +03:00
Gani Georgiev 08b4fc20a9 fixed changelog typo 2023-07-03 19:46:40 +03:00
Gani Georgiev 9ec01d74d8 optimized search count queries to use rowid by default 2023-07-03 17:57:23 +03:00
Gani Georgiev 1679c88e6d updated binder test cases 2023-06-26 13:04:15 +03:00
Gani Georgiev 91bbbc4bdb added eager empty string check 2023-06-26 12:53:15 +03:00
Gani Georgiev 4ab9c6f87f updated ui/dist 2023-06-26 12:50:21 +03:00
Gani Georgiev 68157a3a65 added missing array value normalization 2023-06-26 12:47:56 +03:00
Gani Georgiev e6cf4ad2ef updated go deps 2023-06-26 12:44:14 +03:00
Gani Georgiev edd2eaae88 added negative number test 2023-06-26 12:42:53 +03:00
Gani Georgiev 2e8e835a68 updated ui/dist 2023-06-26 12:33:55 +03:00
Gani Georgiev 9cba6ac386 [#2763] fixed multipart/form-data array value bind 2023-06-26 12:30:51 +03:00
Gani Georgiev bd94940eef updated changelog 2023-06-16 14:47:34 +03:00
Sven-Kristjan Kompus caf343ef9c [#2726] removed unnecessary Dao().TotalAdmins() call 2023-06-16 14:43:05 +03:00
Gani Georgiev d1bcaa65b1 updated changelog 2023-06-10 23:34:32 +03:00
Gani Georgiev 2d60ad170e updated ui/dist 2023-06-10 23:32:41 +03:00
Simon Loir 0ba963a5d7 [#2681] fixed collection index column sort normalization 2023-06-10 23:30:32 +03:00
Gani Georgiev 87a6f1bebb updated deps and removed unnecessary normalizeSort() call 2023-06-09 13:32:01 +03:00
Gani Georgiev aa5b5bb83e added default initial -created sort 2023-06-09 13:07:34 +03:00
Gani Georgiev af193d440d [#2675] fixed implicit relation display fields serialization 2023-06-09 12:57:17 +03:00
Gani Georgiev 7f06816008 normalized active sort on collection schema change 2023-06-09 12:56:07 +03:00
Gani Georgiev e4a90f6605 updated npm deps and generated ui/dist 2023-06-02 19:41:13 +03:00
Gani Georgiev acbce42bff updated changelog 2023-06-02 19:33:20 +03:00
Gani Georgiev b33bebd4cd bumped app version 2023-06-02 19:32:26 +03:00
Gani Georgiev 3122223a71 [#2602] added common int types support when scanning types.DateTime 2023-05-30 20:43:20 +03:00
Gani Georgiev dbbc1e25ca replaced multiple error wraps with plain %v for older go 1.18 and go 1.19 compatibility 2023-05-29 17:01:04 +03:00
Gani Georgiev 732044f795 keep both original file and fallback errors 2023-05-28 22:00:48 +03:00
Gani Georgiev 9a1354ae62 [#2589] added .exe fallback to the selfupdate cmd and replaced path with filepath 2023-05-28 21:57:12 +03:00
Gani Georgiev e3876c0e13 updated nocgo busy_timeout pragma 2023-05-27 09:37:21 +03:00
Gani Georgiev b1307c9041 registered custom cgo sqlite driver 2023-05-27 09:24:18 +03:00
Gani Georgiev dcdf43f0fc return conn.Exec error 2023-05-27 09:07:38 +03:00
Gani Georgiev f6a616b7e8 [#2570] fixed default PRAGMAs not being applied for new connections 2023-05-27 09:04:01 +03:00
Gani Georgiev 833a84b3d7 [#2570] use ON for consistency with the SQLite examples and updated collection delete test to check for _exterAuths references 2023-05-25 20:26:03 +03:00
Gani Georgiev c72c951b24 [#2567] fixed schema fields sort not working on Safari/Gnome Web 2023-05-24 23:19:06 +03:00
Gani Georgiev e9969ed6d1 updated changelog 2023-05-24 09:34:23 +03:00
Gani Georgiev 511259ed10 fixed missing view id field error message typo 2023-05-24 09:25:46 +03:00
Gani Georgiev 6b16b7856b [#2551] auto register the initial generated snapshot migration to prevent incorrectly reapplying the snapshot on container restart 2023-05-24 09:23:10 +03:00
Gani Georgiev 231ddc9791 [#2541] removed file field dataTransfer.effectAllowed checks 2023-05-23 21:09:24 +03:00
Gani Georgiev 5bf2f692d8 fixed formatting 2023-05-23 21:08:22 +03:00
Gani Georgiev ce28a9af78 [#2548] use fileath.Clean on the fs.WalkDirFunc argument to ensure that the same normalizations are applied 2023-05-23 18:34:24 +03:00
Gani Georgiev db20e38cda updated backups test to ensure that the backups dir is not part of the generated zip 2023-05-23 16:47:58 +03:00
Gani Georgiev 286046e15a improved update cmd version check 2023-05-23 11:35:54 +03:00
Gani Georgiev 4440b5f817 updated changelog 2023-05-23 10:32:31 +03:00
Gani Georgiev d7745ba702 [#2541] fixed file field drag and drop not working in Firefox and Safari 2023-05-23 10:23:41 +03:00
Gani Georgiev 007ef1e152 [#2540] fixed missing CommonHelper references on Downloaad as JSON btn 2023-05-23 07:55:21 +03:00
Gani Georgiev a291cb5ca7 [#2535] avoid mutating the cached request data on OAuth2 user create 2023-05-22 23:59:36 +03:00
Gani Georgiev 86049ed048 updated changelog 2023-05-22 08:02:03 +03:00
Gani Georgiev 6457fc794f updated ui/dist 2023-05-22 07:59:56 +03:00
Gani Georgiev 0eecbd8289 updated app.RestoreBackup() godoc 2023-05-22 07:58:44 +03:00
David Schissler 6521decfa8 [#2526] fixed typo in BackupCreatePanel.svelte 2023-05-22 07:50:42 +03:00
Kunal Singh c370d84074 [#2527] removed unnecessary slice check 2023-05-22 07:48:22 +03:00
Gani Georgiev 4f5aa6ffda updated changelog and ui/dist 2023-05-21 21:06:20 +03:00
Frangu Madalin 8921712821 [#2523] fixed realtime dart api preview example 2023-05-21 21:00:24 +03:00
Gani Georgiev 1c63ae1324 [#2519] replace os.Rename with manually moving the dir children 2023-05-21 20:46:47 +03:00
Gani Georgiev cbaca91581 ignore the v prefix when comparing the tag and the current version 2023-05-21 13:00:12 +03:00
Gani Georgiev 5551f8f5aa eager update app settings and added isServe check for the auto backups 2023-05-21 11:47:05 +03:00
Gani Georgiev a56a04ed0e updated deps 2023-05-21 10:55:33 +03:00
Gani Georgiev 47bb0bc2d8 updated the verification api preview example 2023-05-20 23:53:04 +03:00
Gani Georgiev f49e90bd0b added fields query param to the api docs preview 2023-05-20 06:03:30 +03:00
Gani Georgiev 3820f3d7d7 updated page sidebar width styles 2023-05-19 13:48:58 +03:00
Gani Georgiev faaf19d3bb [#2510] added missing .length prop to the select searchable check 2023-05-19 13:46:30 +03:00
Gani Georgiev 6d0303deaf added meta.isNew to the json OAuth2 auth response 2023-05-18 00:19:54 +03:00
Gani Georgiev d36a044dc7 updated npm deps 2023-05-17 23:59:54 +03:00
Gani Georgiev 1cc67ef7e3 added sorting for the multiple relations form field 2023-05-17 23:54:06 +03:00
Gani Georgiev 1e2d1045b8 added title attrbite to the collection sidebar items and increased the default width 2023-05-17 23:33:43 +03:00
Gani Georgiev df5a291d20 use the filenames instead of indexes on delete 2023-05-17 23:17:45 +03:00
Gani Georgiev 04e0ad9b21 [#2445] added support for multiple files sort in the Admin UI 2023-05-17 22:41:42 +03:00
Gani Georgiev 24ab233376 added experimental update command 2023-05-17 21:14:12 +03:00
Gani Georgiev a42ab6a205 removed the legacy temp upgrade command 2023-05-17 18:39:15 +03:00
Gani Georgiev 472671fee1 fixed comment typo and updated default fallback displayable props 2023-05-16 09:35:33 +03:00
Gani Georgiev 6bde84131c [#2466] added accept file field attr 2023-05-13 22:23:19 +03:00
Gani Georgiev e8b4a7eb26 added backup apis and tests 2023-05-13 22:12:42 +03:00
Gani Georgiev 3b0f60fe15 added basic cron util 2023-05-09 22:36:40 +03:00
Gani Georgiev d3314e1e23 (untested!) added temp backup api scaffoldings before introducing autobackups and rotations 2023-05-08 21:52:40 +03:00
Gani Georgiev 60eee96034 synced with master 2023-05-05 08:49:21 +03:00
Gani Georgiev c6d5992442 updated changelog and ui/dist 2023-05-05 05:52:48 +03:00
Gani Georgiev a7f3805f87 [#2423] insert default settings params with the init migration 2023-05-05 05:22:00 +03:00
Gani Georgiev 92f21e3355 trigger the OnTerminate hook and use a buffered chan instead of wg 2023-05-04 15:12:28 +03:00
Gani Georgiev 4c57968d89 synced with master 2023-05-03 14:52:10 +03:00
Gani Georgiev b84ebabf4a [#2410] Fixed editor field fullscreen z-index 2023-05-03 14:37:19 +03:00
Gani Georgiev 90abe1612e added helper archive package to create and extract zips 2023-05-02 14:51:15 +03:00
Gani Georgiev dfabfa779e synced with master 2023-05-01 22:07:17 +03:00
Gani Georgiev 28c9f02cb4 updated js sdk and resolved isNew record field conflict 2023-05-01 13:00:12 +03:00
Gani Georgiev 67e1974228 updated js sdk 2023-04-28 16:22:38 +03:00
Gani Georgiev 0dcd332962 temporary change the base backend env url 2023-04-28 15:55:26 +03:00
Gani Georgiev f70b457507 updated ui/dist 2023-04-28 15:47:48 +03:00
Gani Georgiev c8fd831115 fixed changelog typos 2023-04-27 20:53:49 +03:00
Gani Georgiev beca0a044e changed X-Forwarded-For parsing to use the first non-empty leftmost-ish ip as it is more close to the 'real ip' 2023-04-27 20:52:08 +03:00
Gani Georgiev 9fa56b020c [#2372] use Fly-Client-IP header if available for the 'real' user ip 2023-04-27 20:29:59 +03:00
Gani Georgiev 429929dd0c added explicit readonly and disabled props to MultipleValueInput 2023-04-27 08:30:04 +03:00
Gani Georgiev ecf7d657c0 updated npm deps and added border to the expanded schema field 2023-04-26 22:15:41 +03:00
Gani Georgiev 4296148c87 updated ui/dist 2023-04-26 18:55:57 +03:00
Gani Georgiev bfb38ab51e added select readonly prop and updated the disabled schema field state 2023-04-26 18:35:34 +03:00
Gani Georgiev 1967dcfeba updated schema field options dropdown spacing 2023-04-26 18:04:08 +03:00
Gani Georgiev 5c95e9b109 [#2318] updated schema fields ui 2023-04-26 14:12:47 +03:00
Gani Georgiev 39c3a95a08 Merge branch 'master' into develop 2023-04-25 19:18:42 +03:00
Gani Georgiev 4944884683 updated changelog 2023-04-25 18:11:04 +03:00
Gani Georgiev c0a7d0f6c0 added ?fields query parameter support to limit the returned api fields 2023-04-25 17:58:51 +03:00
Gani Georgiev 841a4b6913 synced with master 2023-04-25 12:40:59 +03:00
Gani Georgiev 7ce21cc4d9 Merge branch 'master' into develop 2023-04-24 16:04:45 +03:00
Gani Georgiev 7acd90498c synced with master 2023-04-24 13:25:47 +03:00
Gani Georgiev f2a011d63f [#2334] removed first char lowercase normalization to comply with the common backend regex 2023-04-21 19:03:59 +03:00
Gani Georgiev b31c2ceffa synced with master 2023-04-21 11:53:20 +03:00
Gani Georgiev d8f9b52011 updated go deps 2023-04-20 23:45:51 +03:00
Gani Georgiev faa0a9f7dc added admin console command tests 2023-04-20 23:39:48 +03:00
Gani Georgiev 4d94673839 Merge branch 'master' into develop 2023-04-20 21:18:49 +03:00
Gani Georgiev 025c9313e1 added admin console command 2023-04-20 17:20:56 +03:00
Gani Georgiev 0b023b2c02 Merge branch 'master' into develop 2023-04-20 16:24:46 +03:00
Gani Georgiev 3358d8476b added apis.Serve helper 2023-04-20 05:06:22 +03:00
Gani Georgiev 060ed8013e synced with master 2023-04-19 14:09:06 +03:00
Gani Georgiev d69e72deaf updated changelog 2023-04-18 07:09:07 +03:00
209 changed files with 9693 additions and 3269 deletions

No files matched your search

+151 -2
View File
@@ -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
View File
@@ -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)
}
+571
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+2
View File
@@ -16,6 +16,8 @@ func TestHealthAPI(t *testing.T) {
ExpectedStatus: 200,
ExpectedContent: []string{
`"code":200`,
`"data":{`,
`"canBackup":true`,
},
},
}
+13 -6
View File
@@ -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
View File
@@ -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)
})
}
})
+5
View File
@@ -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))
}
+3 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+219
View File
@@ -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
View File
@@ -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
}
-444
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
+325
View File
@@ -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
}
+159
View File
@@ -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
}
+30
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+4
View File
@@ -65,6 +65,10 @@ type BootstrapEvent struct {
App App
}
type TerminateEvent struct {
App App
}
type ServeEvent struct {
App App
Router *echo.Echo
+25 -4
View File
@@ -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
View File
@@ -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
+4
View File
@@ -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
+79
View File
@@ -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...)
}
+102
View File
@@ -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)
}
}()
}
}
+1 -1
View File
@@ -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
View File
@@ -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
}
}
}
}
+15 -2
View File
@@ -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...)
}
+44 -42
View File
@@ -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)
}
}()
}
}
+88
View File
@@ -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
}
+103
View File
@@ -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)
}
}
}
+46 -45
View File
@@ -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
+611 -184
View File
File diff suppressed because it is too large. Load diff
+11 -1
View File
@@ -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",
+20
View File
@@ -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
})
}
+9
View File
@@ -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"`
}
+1 -1
View File
@@ -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()...)
+9
View File
@@ -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{
+58 -6
View File
@@ -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"`
+70
View File
@@ -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
+372
View File
@@ -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
}
+32
View File
@@ -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)
}
}
}
+35
View File
@@ -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)
}
+23
View File
@@ -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)
}
}
+40 -24
View File
@@ -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)
+36
View File
@@ -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
View File
@@ -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
+9
View File
@@ -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.
+87
View File
@@ -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
})
}
+120
View File
@@ -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
}
+72
View File
@@ -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
}
+87
View File
@@ -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)
}
}
+173
View File
@@ -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()
}
}
}
+220
View File
@@ -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)
}
}
+194
View File
@@ -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
}
+361
View File
@@ -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)
}
}
}
+1 -1
View File
@@ -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))?$`)
)
+13 -2
View File
@@ -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",
+27
View File
@@ -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{
+61
View File
@@ -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)
+2
View File
@@ -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()
-3
View File
@@ -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 {
-4
View File
@@ -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
}
+80
View File
@@ -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
}
+144
View File
@@ -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
}
+121
View File
@@ -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)
}
}
+195
View File
@@ -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)
}
}
}
+75 -3
View File
@@ -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
}
}
+28 -21
View File
@@ -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)
}
}
}
+10 -1
View File
@@ -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
}
+19 -5
View File
@@ -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",
},
},
+1 -1
View File
@@ -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 == "" {
+5
View File
@@ -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"},
}
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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};
@@ -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
View File
@@ -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!==(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
View File
@@ -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&&lt(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)&&lt(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
View File
@@ -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
View File
@@ -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
@@ -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.",
@@ -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.",
+62
View File
@@ -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};
-50
View File
@@ -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
View File
@@ -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 &quot;Manage&quot; 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
View File
@@ -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 &quot;Manage&quot; 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};
@@ -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
View File
@@ -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 @@
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