Compare commits

...
99 Commits
Author SHA1 Message Date
Gani Georgiev 78a35a339f bumped app version 2024-12-18 18:27:42 +02:00
Gani Georgiev c97af83ed1 downgraded modernc,org/libc to 1.55.3 2024-12-18 18:08:03 +02:00
Gani Georgiev 803941705c [#6137] renew the superuser file token cache when clicking on the thumb preview or download link 2024-12-18 16:16:53 +02:00
Gani Georgiev ef0170cf0b [#6132] replaced strconv.Itoa with strconv.FormatInt where it could overflow 2024-12-17 21:41:55 +02:00
Gani Georgiev 9b4200a65c updated deps and changelog 2024-12-16 15:04:19 +02:00
Gani Georgiev cb3936a499 [#6122] fixed model->record and model->collection events sync 2024-12-16 14:49:24 +02:00
Gani Georgiev e34c25858c added extra html escaping for the RECORD:* placeholders as extra measure in case the email are stored as plain html 2024-12-16 14:48:44 +02:00
Gani Georgiev 3098c2dcd8 [#6102] fixed JSVM exception -> Go error unwrapping 2024-12-13 17:57:14 +02:00
Gani Georgiev 4f35fb74c8 updated jsvm types 2024-12-11 19:00:45 +02:00
Gani Georgiev f533320722 updated go deps and bumped app version 2024-12-11 19:00:28 +02:00
Gani Georgiev 9efd68ff4c [#6066] updated text field validation message 2024-12-09 12:24:12 +02:00
Gani Georgiev 9747f46c1d [#6063] fixed x-forwarded-for typo 2024-12-09 04:39:14 +02:00
Gani Georgiev 88a1867169 [#6058] fixed filesystem.fileFromURL documentation and generated type 2024-12-08 17:51:30 +02:00
Gani Georgiev 545a4eb47c updated ui/dist 2024-12-08 14:00:07 +02:00
Gani Georgiev 5f660d8671 updated go deps and min go release action version 2024-12-08 13:52:10 +02:00
Gani Georgiev 85c31ba068 [#6053] fixed text field max validation error message 2024-12-08 13:23:18 +02:00
Gani Georgiev 6f55695fa1 updated jsvm types 2024-12-06 14:21:22 +02:00
Gani Georgiev c6695b6a75 updated min go version and removed legacy CGO note from the readme 2024-12-04 11:12:10 +02:00
Gani Georgiev b0276ad605 [#6019] fixed README example 2024-12-04 11:06:00 +02:00
Gani Georgiev 376627b4cc fixed logs search 2024-12-03 12:39:27 +02:00
Gani Georgiev 0661de0604 updated go deps 2024-12-02 14:04:11 +02:00
Gani Georgiev f80de83234 updated ui/dist 2024-12-02 14:03:16 +02:00
Gani Georgiev 2d828ef9eb added more descriptive test OTP id and failure log message 2024-12-02 13:46:29 +02:00
Gani Georgiev 77ac44a49a moved the default UI CSP as response header 2024-12-02 13:45:44 +02:00
Gani Georgiev fb2763a697 updated autodate test interceptors 2024-12-02 12:51:25 +02:00
Gani Georgiev 5835a51111 [#6000] fixed autodate interceptors 2024-12-02 12:39:02 +02:00
Gani Georgiev 3c2d43d37b updated npm deps 2024-11-29 11:59:33 +02:00
Gani Georgiev 6ee25cbe12 minimized repeated field.GetName calls 2024-11-29 11:30:54 +02:00
Gani Georgiev 67e6be8073 updated ui/dist 2024-11-29 11:19:15 +02:00
Gani Georgiev d69e81922f updated Record.Fresh and Record.Clone tests 2024-11-29 11:10:27 +02:00
Gani Georgiev 51ac522e7f [#5973] fixed Record.Fresh and Record.Clone method not properly copying the record fields 2024-11-28 23:19:21 +02:00
Gani Georgiev bcb2dca44e updated go deps 2024-11-28 16:24:21 +02:00
Gani Georgiev 79f6f4ee60 fixed comment typo 2024-11-28 15:21:19 +02:00
Gani Georgiev ab7194a639 fixed gzip middleware not applying when serving static files 2024-11-28 13:51:43 +02:00
Gani Georgiev d92016af81 updated changelog 2024-11-27 10:02:11 +02:00
Gani Georgiev 6b9d1b559c updated ui/dist 2024-11-26 22:34:23 +02:00
Gani Georgiev 44f097f7d2 updated jsvm types 2024-11-26 22:31:46 +02:00
Gani Georgiev fcf65dcc77 added collection rules change list in the confirmation popup 2024-11-26 22:28:34 +02:00
Gani Georgiev 06acaf38d8 fixed dev log query print formatting 2024-11-26 20:58:35 +02:00
Gani Georgiev cb2b27f6ed updated otp request error message 2024-11-26 19:51:21 +02:00
Gani Georgiev 6e26cb5d88 [#5958] fixed RecordQuery() custom struct scanning 2024-11-26 14:15:39 +02:00
Gani Georgiev f1b199b35c added support for passing more than one id in the Hook.Unbind method for consistency with the router 2024-11-26 11:52:26 +02:00
Gani Georgiev 0ac4a388c0 updated changelog 2024-11-25 11:39:26 +02:00
Gani Georgiev aff8abccc7 updated changelog 2024-11-25 11:36:07 +02:00
Gani Georgiev 51dfbc251a updated jsvm types 2024-11-25 11:30:59 +02:00
Gani Georgiev c933190db8 updated ui/dist 2024-11-25 11:30:01 +02:00
Gani Georgiev fab334fca6 updated error messages 2024-11-25 11:28:20 +02:00
Gani Georgiev 5a5211d7f2 [#5940] added blob response write helper 2024-11-25 09:11:58 +02:00
Gani Georgiev 8f6e91c485 added note about the trailing slash cahnge in the changelog 2024-11-24 22:10:24 +02:00
Gani Georgiev 64fa4b9cef updated changelog link 2024-11-24 21:14:54 +02:00
Gani Georgiev d5dddf3ead updated PocketBase JS SDK version 2024-11-24 16:37:01 +02:00
Gani Georgiev 62a4795a4c exclude autogenerated changelog htmls from git 2024-11-24 16:20:34 +02:00
Gani Georgiev fd18eb520f updated go deps 2024-11-24 16:14:51 +02:00
Gani Georgiev 73361370f0 added list hidden field filter test 2024-11-24 15:35:42 +02:00
Gani Georgiev ef71daae65 autofocus the installer email field 2024-11-24 15:02:48 +02:00
Gani Georgiev 5936fc3ac3 add modified fields with order in the generated migration 2024-11-24 15:02:28 +02:00
Gani Georgiev 0efca0f936 updated ui/dist and changelog 2024-11-24 12:48:03 +02:00
Gani Georgiev 1e92b51cf7 added option to insert/move fields at specific position 2024-11-24 12:41:57 +02:00
Gani Georgiev e9ece220d6 added env variables support for the thumbs generation limits 2024-11-23 20:38:30 +02:00
Gani Georgiev 2dd4e38e1d fixed superuser OTP id input reactivity, enabled truncate dropdown option for system collections, updated jsvm types 2024-11-23 14:12:18 +02:00
Gani Georgiev b34453afc7 show the OTP id in the superuser form 2024-11-23 12:44:03 +02:00
Gani Georgiev c3b347af4b added note and tests regarding the shared batch Authorization header 2024-11-23 11:54:25 +02:00
Gani Georgiev 68e9c55925 updated checkOldPassword validator 2024-11-23 10:28:31 +02:00
Gani Georgiev e5800875c2 updated ui/dist and added fallback debug log 2024-11-22 23:30:44 +02:00
Gani Georgiev 2e43518bb4 synced ported cors middleware 2024-11-22 23:19:23 +02:00
Gani Georgiev e5f1bc3c37 renamed Monday.FetchRawUserData to Monday.FetchUserInfo 2024-11-22 22:43:16 +02:00
Gani Georgievandchnfyi e3bf81cb79 [#5909] added Linear OAuth2 provider
Co-authored-by: chnfyi <143424481+chnfyi@users.noreply.github.com>
2024-11-22 22:42:04 +02:00
Gani Georgiev 305e183f58 added apis.ToApiError alias 2024-11-22 20:44:29 +02:00
Gani Georgiev 7795a086ef hide batch api preview for view collections 2024-11-22 16:18:52 +02:00
Gani Georgiev 5d8a8dd7d8 updated godoc and renamed cors middleware handler 2024-11-21 22:22:58 +02:00
Gani Georgiev 31d3c27f43 add s390x and ppc64le linux builds 2024-11-21 16:55:27 +02:00
Gani Georgiev c3ac28a8e6 updated go.mod 2024-11-21 16:33:40 +02:00
Gani Georgiev ab26941d3f enabled Buffer goja_nodejs module 2024-11-21 16:29:42 +02:00
Gani Georgiev a18df14f4f updated js sdk and other npm deps 2024-11-21 12:24:04 +02:00
Gani Georgiev c2e7ab8d41 fixed oauth2 redirect test 2024-11-21 12:11:00 +02:00
Gani Georgiev 7ee6b11e9d return an error in case of required MFA so that external handlers can react if necessary 2024-11-21 11:12:25 +02:00
Gani Georgiev 8ab02ce402 updared rate limiter sort rules 2024-11-21 11:11:51 +02:00
Gani Georgiev 779f6c1a74 added record json copy option 2024-11-20 21:04:42 +02:00
Gani Georgiev 34d7ac0808 close channel on client discard 2024-11-19 23:23:25 +02:00
Gani Georgiev d0795bd849 updated tests 2024-11-19 22:37:44 +02:00
Gani Georgiev 08f2190ad1 [#5898] instead of unregister, unset the realtime client auth state on delete of the related auth record 2024-11-19 22:36:32 +02:00
Gani Georgiev d919d55b5e allow mixing existing file names and new uploaded files 2024-11-19 17:45:15 +02:00
Gani Georgiev 52c64318c5 fixed comment typo 2024-11-19 17:27:18 +02:00
Gani Georgiev 9fe4f87e5b added required validator for the TextField.Pattern option in case it is a primary key 2024-11-19 17:21:43 +02:00
Gani Georgiev 48328bf33f added extra validations in case of id pattern validator misuse 2024-11-19 15:59:59 +02:00
Gani Georgiev 52e85a8036 added oauth2 db errors handling and replaced the auth response map with a struct 2024-11-18 21:16:20 +02:00
Gani Georgiev 37538f2a6d auto sort rate limit rules 2024-11-18 16:07:52 +02:00
Gani Georgiev 70df03ffbb fixed rate limiter rules matching to acount for the Audience field 2024-11-18 14:46:06 +02:00
Gani Georgiev 487e83c84e fixed typo and cleanup destroyed overlay wrappers 2024-11-18 08:23:39 +02:00
Gani Georgiev 14aaeb06fc fixed responsive styles of the OAuth2 providers list 2024-11-18 07:51:19 +02:00
Gani Georgiev 19ea9cca36 removed old system created and updated fields from the record preview 2024-11-18 05:56:29 +02:00
Gani Georgiev 6090b29070 [#5887] fixed record duplicate and removed the duplicated id field in the record preview 2024-11-18 05:53:20 +02:00
Gani Georgiev 957ba6266d updated changelogs 2024-11-17 15:54:23 +02:00
Gani Georgiev e1472d922a updated ui/dist 2024-11-17 15:38:38 +02:00
Gani Georgiev 3653e70a6d added periodic wal_checkpoint calls 2024-11-17 14:26:42 +02:00
Gani Georgiev 7f2fcc0046 added manual WAL checkpoints before creating the zip backup to avoid copying unnecessary data 2024-11-17 12:33:23 +02:00
Gani Georgiev 32e0eadc2f updated godoc links 2024-11-16 18:57:00 +02:00
Gani Georgiev 18a7549e50 updated collection indexes on system fields validator and normalized v0.23 old collections migration 2024-11-15 09:13:00 +02:00
Gani Georgiev e53c30ca4d added 'table is locked' error msg retry check 2024-11-15 07:45:27 +02:00
178 changed files with 8572 additions and 5925 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '>=1.23.0'
go-version: '>=1.23.4'
# This step usually is not needed because the /ui/dist is pregenerated locally
# but its here to ensure that each release embeds the latest admin ui artifacts.
+2
View File
@@ -15,4 +15,6 @@ coverage.out
# generated markdown previews
README.html
CHANGELOG.html
CHANGELOG_16_22.html
CHANGELOG_8_15.html
LICENSE.html
+10
View File
@@ -24,13 +24,23 @@ builds:
- amd64
- arm64
- arm
- s390x
- ppc64le
goarm:
- 7
ignore:
- goos: windows
goarch: arm
- goos: windows
goarch: s390x
- goos: windows
goarch: ppc64le
- goos: darwin
goarch: arm
- goos: darwin
goarch: s390x
- goos: darwin
goarch: ppc64le
release:
draft: true
+70 -272
View File
@@ -1,329 +1,123 @@
## v0.23.0-rc13
## v0.23.10
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
- Renew the superuser file token cache when clicking on the thumb preview or download link ([#6137](https://github.com/pocketbase/pocketbase/discussions/6137)).
- Added WakaTime OAuth2 provider ([#5829](https://github.com/pocketbase/pocketbase/pull/5829); thanks @tigawanna).
- Upgraded `modernc.org/sqlite` to 1.34.3 to fix "disk io" error on arm64 systems.
_If you are extending PocketBase with Go and upgrading with `go get -u` make sure to manually set in your go.mod the `modernc.org/libc` indirect dependency to v1.55.3, aka. the exact same version the driver is using._
- Added `superuser otp EMAIL` command as fallback for generating superuser OTPs from the command line in case OTP has been enabled for the `_superusers` but the SMTP server has deliverability issues.
- ⚠️ Changed `OnRecordRequestOTPRequest` hook to be triggered even if there is no record matching the email (aka. `e.Record` could be `nil`), allowing you to manually create a new user with the OTP request and assigning it to `e.Record`.
## v0.23.9
- Added new `sentTo` system field to the `_otps` collection (it is managed programmatically or by superusers; it could be anything - email, phone, messanger app id, etc.).
By default when the OTP is submitted via email it is automatically populated with the user email address (via the `OnMailerRecordOTPSend` hook's finalizer).
This allow us on valid `auth-with-otp` request to automatically mark the user email as verified.
- Replaced `strconv.Itoa` with `strconv.FormatInt` to avoid the int64->int conversion overflow on 32-bit platforms ([#6132](https://github.com/pocketbase/pocketbase/discussions/6132)).
- Added `RateLimitRule.Audience` optional field for restricting a rate limit rule for `"@guest"`-only, `"@auth"`-only, `""`-any (default).
- Added default max limits for the expressions count and length of the search filter and sort params.
_This is just an extra measure mostly for the case when the filter and sort parameters are resolved outside of the request context since the request size limits won't apply._
## v0.23.8
- Other minor changes (better error in case of duplicated rate limit rule, fixed typos, changed field id and name length validator to max 100, updated Go deps, etc.).
- Fixed Model->Record and Model->Collection hook events sync for nested and/or inner-hook transactions ([#6122](https://github.com/pocketbase/pocketbase/discussions/6122)).
- Other minor improvements (updated Go and npm deps, added extra escaping for the default mail record params in case the emails are stored as html files, fixed code comment typos, etc.).
## v0.23.0-rc12
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
## v0.23.7
- Fixed validation error caused by the id change in v0.23.0-rc10 migration ([#5820](https://github.com/pocketbase/pocketbase/discussions/5820)).
- Fixed JSVM exception -> Go error unwrapping when throwing errors from non-request hooks ([#6102](https://github.com/pocketbase/pocketbase/discussions/6102)).
## v0.23.0-rc11
## v0.23.6
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
- Fixed `$filesystem.fileFromURL` documentation and generated type ([#6058](https://github.com/pocketbase/pocketbase/issues/6058)).
- Fixed JSVM types errors ([#5797](https://github.com/pocketbase/pocketbase/issues/5797)).
- Fixed `X-Forwarded-For` header typo in the suggested UI "Common trusted proxy" headers ([#6063](https://github.com/pocketbase/pocketbase/pull/6063)).
- Skip the default `loadAuthToken` middleware if `e.Auth` is already loaded ([#5800](https://github.com/pocketbase/pocketbase/discussions/5800)).
- Updated the `text` field max length validator error message to make it more clear ([#6066](https://github.com/pocketbase/pocketbase/issues/6066)).
- Restored the API rules for the default initial "users" collection migration to be the same as in <= v0.22.x.
- Other minor fixes (updated Go deps, skipped unnecessary validator check when the default primary key pattern is used, updated JSVM types, etc.).
- ⚠️ Replaced the old Instagram provider with a new one that is compatible with the new Instagram Login APIs ([#5588](https://github.com/pocketbase/pocketbase/pull/5588); thanks @pnmcosta).
_The provider key is `instagram2` to prevent conflicts with existing linked users._
- ⚠️ Changes regarding the first superuser creation based on [#5814](https://github.com/pocketbase/pocketbase/discussions/5814).
## v0.23.5
- ⚠️ Removed `apis.RequireSuperuserAuthOnlyIfAny()` middleware.
- Fixed UI logs search not properly accounting for the "Include requests by superusers" toggle when multiple search expressions are used.
- ⚠️ Removed `RequestEvent.UnsafeRealIP()` to prevent misuse and confusion with `RequestEvent.RealIP()` (_the latter is considered safer because it checks the trusted proxy headers settings_).
- Fixed `text` field max validation error message ([#6053](https://github.com/pocketbase/pocketbase/issues/6053)).
- Other minor fixes (comment typos, JSVM types update).
## v0.23.0-rc10
- Updated Go deps and the min Go releleaser GitHub action version to 1.23.4.
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
- Restore the CRC32 checksum autogeneration for the collection/field ids in order to maintain deterministic default identifier value and minimize conflicts between custom migrations and full collections snapshots.
_There is a system migration that will attempt to normalize existing system collections ids, but if you already migrated to v0.23.0-rc and have generated a full collections snapshot migration, you have to delete it and regenerate a new one._
## v0.23.4
- Change the behavior of the default generated collections snapshot migration to act as "extend" instead of "replace" to prevent accidental data deletion.
_I think this would be rare but if you want the old behaviour you can edit the generated snapshot file and replace the second argument (`deleteMissing`) of `App.ImportCollection/App.ImportCollectionsByMarshaledJSON` from `false` to `true`._
- Fixed `autodate` fields not refreshing when calling `Save` multiple times on the same `Record` instance ([#6000](https://github.com/pocketbase/pocketbase/issues/6000)).
- Added `app.SubscriptionsBroker().TotalClients()` helper method to return the total registered realtime clients ([#5793](https://github.com/pocketbase/pocketbase/issues/5793)).
- Added more descriptive test OTP id and failure log message ([#5982](https://github.com/pocketbase/pocketbase/discussions/5982)).
- Moved the default UI CSP from meta tag to response header ([#5995](https://github.com/pocketbase/pocketbase/discussions/5995)).
## v0.23.0-rc9
- Updated Go and npm dependencies.
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
- Fixed auto `www.` redirect due to missing URI schema.
## v0.23.3
- Fixed collection and field renaming when reusing an old collection/field name ([#5741](https://github.com/pocketbase/pocketbase/issues/5741)).
- Fixed Gzip middleware not applying when serving static files.
- Update the "API preview" section to include information about the batch api.
- Fixed `Record.Fresh()`/`Record.Clone()` methods not properly cloning `autodate` fields ([#5973](https://github.com/pocketbase/pocketbase/discussions/5973)).
- Exported `core.DefaultDBConnect` function that could be used as a fallback when initializing custom SQLite drivers and builds.
- ⚠️ No longer loads the `mattn/go-sqlite3` driver by default when building with `CGO_ENABLED=1` to avoid `multiple definition ...` linker errors in case different CGO SQLite drivers or builds are used.
This means that no matter of the `CGO_ENABLED` value, now out of the box PocketBase will always use only the pure Go driver ([`modernc.org/sqlite`](https://pkg.go.dev/modernc.org/sqlite)).
This will be documented properly in the new website but if you want to continue using `mattn/go-sqlite3` (e.g. because of the icu or other builtin extension) you could register it as follow:
```go
package main
## v0.23.2
import (
"database/sql"
"log"
- Fixed `RecordQuery()` custom struct scanning ([#5958](https://github.com/pocketbase/pocketbase/discussions/5958)).
"github.com/mattn/go-sqlite3"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase"
)
- Fixed `--dev` log query print formatting.
func init() {
// initialize default PRAGMAs for each new 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;
PRAGMA temp_store = MEMORY;
PRAGMA cache_size = -16000;
`, nil)
- Added support for passing more than one id in the `Hook.Unbind` method for consistency with the router.
return err
},
},
)
- Added collection rules change list in the confirmation popup
(_to avoid getting anoying during development, the rules confirmation currently is enabled only when using https_).
dbx.BuilderFuncMap["pb_sqlite3"] = dbx.BuilderFuncMap["sqlite3"]
}
func main() {
app := pocketbase.NewWithConfig(pocketbase.Config{
DBConnect: func(dbPath string) (*dbx.DB, error) {
return dbx.Open("pb_sqlite3", dbPath)
},
})
## v0.23.1
// custom hooks and plugins...
- Added `RequestEvent.Blob(status, contentType, bytes)` response write helper ([#5940](https://github.com/pocketbase/pocketbase/discussions/5940)).
if err := app.Start(); err != nil {
log.Fatal(err)
}
}
```
Also note that if you are not planning to use the `core.DefaultDBConnect` fallback as part of your custom driver registration you can exclude the default pure Go driver from the build with the build tag `-tags no_default_driver` to reduce the binary size a little.
- Added more descriptive error messages.
- ⚠️ Removed JSVM `BaseCollection()`, `AuthCollection()`, `ViewCollection()` class aliases for simplicity and to avoid confusion with the accepted constructor arguments (_you can simply use as before `new Collection({ type: "base", ... })`; this will also initialize the default type specific options_).
- Other minor improvements (added validator for duplicated index definitions, updated the impersonate popup styles, added query param support for loading a collection based on its name, etc.).
## v0.23.0-rc8
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
- Lock the `_otps` and `_mfas` system collections Delete API rule for superusers only.
- Reassign in the JSVM executors the global `$app` variable with the hook scoped `e.app` value to minimize the risk of a deadlock when a hook or middleware is wrapped in a transaction.
- Reuse the OAuth2 created user record pointer to ensure that all its following hooks operate on the same record instance.
- Added tags support for the `OnFileTokenRequest` hook.
- Other minor changes (added index for the `_collections` type column, added more detailed godoc for the collection fields and `core.App` methods, fixed flaky record enrich tests, etc.).
## v0.23.0-rc7
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
- Register the default panic-recover middleware after the activity logger so that we can log the error.
- Updated the `RequestEvent.BindBody` FormData type inferring rules to convert numeric strings into float64 only if the resulting minimal number string representation matches the initial FormData string value ([#5687](https://github.com/pocketbase/pocketbase/issues/5687)).
- Fixed the JSVM types to include properly generated function declarations when the related Go functions have shortened/combined return values.
- Reorganized the record table fields<->columns syncing to remove the `PRAGMA writable_schema` usage.
## v0.23.0-rc6
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
- Fixed realtime 403 API error on resubscribe ([#5674](https://github.com/pocketbase/pocketbase/issues/5674)).
- Fixed the auto OAuth2 avatar mapped field assignment when the OAuth2 provider doesn't return an avatar URL ([#5673](https://github.com/pocketbase/pocketbase/pull/5673)).
_In case the avatar retrieval fails and the mapped record field "Required" option is not set, the error is silenced and only logged with WARN level._
- Added `Router.SEARCH(path, action)` helper method for registering `SEARCH` endpoints.
- Changed all builtin middlewares to return `*hook.Handler[*core.RequestEvent]` with a default middleware id for consistency and to allow removal.
Or in other words, replace `.BindFunc(apis.Gzip())` with `.Bind(apis.Gzip())`.
- Updated the JSVM types to reflect the recent changes.
## v0.23.0-rc5
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
- Added Notion OAuth2 provider ([#4999](https://github.com/pocketbase/pocketbase/pull/4999); thanks @s-li1).
- Added monday.com OAuth2 provider ([#5346](https://github.com/pocketbase/pocketbase/pull/5346); thanks @Jaytpa01).
- Added option to retrieve the OIDC OAuth2 user info from the `id_token` payload for the cases when the provider doesn't have a dedicated user info endpoint.
- Fixed the relation record picker to sort by default by `@rowid` instead of the `created` field as the latter is optional ([#5641](https://github.com/pocketbase/pocketbase/discussions/5641)).
- Fixed the UI "Set Superusers only" button click not properly resetting the input state.
- Fixed the OAuth2 providers logo path shown in the "Authorized providers" UI.
- Fixed the single value UI for the `select`, `file` and `relation` fields ([#5646](https://github.com/pocketbase/pocketbase/discussions/5646))
## v0.23.0-rc4
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
- Fixed the UI settings update form to prevent sending empty string for the mail password or the S3 secret options on resave of the form.
- ⚠️ Added an exception for the `OAuth2` field in the GO->JSVM name mapping rules:
```
// old -> new
collection.oAuth2.* -> collection.oauth2.*
```
- Added more user friendly view collection truncate error message.
- Added an extra suffix character to the name of autogenerated template migration file for `*test` suffixed collections to prevent acidentally resulting in `_test.go` migration files.
- Added `FieldsList.AddMarshaledJSON([]byte)` helper method to load a serialized json array of objects or a single json object into an existing collection fields list.
- Fixed the autogenerated Go migration template when updating a collection ([#5631](https://github.com/pocketbase/pocketbase/discussions/5631)).
⚠️ If you have already used a previous prerelease and have autogenerated Go migration files, please check the migration files named **`{timestamp}_updated_{collection}.go`** and manually change:
<table width="100%">
<tr>
<th width="50%">Old (broken)</th>
<th width="50%">New</th>
</tr>
<tr>
<td width="50%">
```go
// add field / update field
if err := json.Unmarshal([]byte(`[{
...
}]`), &collection.Fields); err != nil {
return err
}
```
</td>
<td width="50%">
```go
// add field / update field
if err := collection.Fields.AddMarshaledJSON([]byte(`{
...
}`)); err != nil {
return err
}
```
</td>
</tr>
</table>
To test that your Go migration files work correctly you can try to start PocketBase with a new temp pb_data, e.g.:
```go
go run . serve --dir="pb_data_temp"
```
## v0.23.0-rc3
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
- Make `PRAGMA optimize` statement optional in case it is not supported by the driver ([#5611](https://github.com/pocketbase/pocketbase/discussions/5611)).
- Reapply the minimum required `pb_data/auxiliary.db` migrations if the db file was manually deleted ([#5618](https://github.com/pocketbase/pocketbase/discussions/5618)).
- To avoid confusion and unnecessary casting, the `hook.HandlerFunc[T]` type has been removed and instead everywhere we now use directly the underlying function definition, aka.:
```go
func(T) error
```
- Fixed the UI input field type of the OTP.length field ([#5617](https://github.com/pocketbase/pocketbase/issues/5617)).
- Other minor fixes (fixed API preview and examples error message typos, better hint for combined/multi-spaced view query columns, fixed the path for the HTTPS green favicon path, etc.).
## v0.23.0-rc2
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
- Small update to the earlier v0.23.0-rc that uses `pb_data/auxiliary.db` instead of `pb_data/aux.db` because it seems that on Windows `aux` is disallowed as file name ([#5607](https://github.com/pocketbase/pocketbase/issues/5607)).
_If you have already upgraded to v0.23.0-rc please rename manually your `pb_data/aux.db` file to `pb_data/auxiliary.db`._
## v0.23.0-rc
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
>
> It introduces many Go/JSVM breaking changes and requires manual migration steps.
>
> All new features will be reflected in the new website documentation with the final v0.23.0 release.
## v0.23.0
> [!NOTE]
> Please note that you don't have to upgrade to PocketBase v0.23.0 if you are not planning further developing
> You don't have to upgrade to PocketBase v0.23.0 if you are not planning further developing
> your existing app and/or are satisfied with the v0.22.x features set. There are no identified critical issues
> with PocketBase v0.22.x yet and in the case of critical bugs and security vulnerabilities, the fixes
> will be backported for at least until Q1 of 2025 (_if not longer_).
>
> If you don't plan upgrading just make sure to pin the SDKs version to their latest PocketBase v0.22.x compatible:
> **If you don't plan upgrading make sure to pin the SDKs version to their latest PocketBase v0.22.x compatible:**
> - JS SDK: `<0.22.0`
> - Dart SDK: `<0.19.0`
PocketBase v0.23.0-rc is a major refactor of the internals with the overall goal of making PocketBase an easier to use Go framework.
> [!CAUTION]
> This release introduces many Go/JSVM and Web APIs breaking changes!
>
> Existing `pb_data` will be automatically upgraded with the start of the new executable,
> but custom Go or JSVM (`pb_hooks`, `pb_migrations`) and JS/Dart SDK code will have to be migrated manually.
> Please refer to the below upgrade guides:
> - Go: https://pocketbase.io/v023upgrade/go/.
> - JSVM: https://pocketbase.io/v023upgrade/jsvm/.
>
> If you had already switched to some of the earlier `<v0.23.0-rc14` versions and have generated a full collections snapshot migration (aka. `./pocketbase migrate collections`), then you may have to regenerate the migration file to ensure that it includes the latest changes.
There are many changes but to highlight some of the most notable ones:
PocketBase v0.23.0 is a major refactor of the internals with the overall goal of making PocketBase an easier to use Go framework.
There are a lot of changes but to highlight some of the most notable ones:
- New and more [detailed documentation](https://pocketbase.io/docs/).
_The old documentation could be accessed at [pocketbase.io/old](https://pocketbase.io/old/)._
- Replaced `echo` with a new router built on top of the Go 1.22 `net/http` mux enhancements.
- Merged `daos` packages in `core.App` to simplify the DB operations (_the `models` package structs are also migrated in `core`_).
- Option to specify custom `DBConnect` function as part of the app configuration to allow different `database/sql` SQLite drivers (_turso/libsql, sqlcipher, etc._) and custom builds.
_Note that we no longer loads the `mattn/go-sqlite3` driver by default when building with `CGO_ENABLED=1` to avoid `multiple definition` linker errors in case different CGO SQLite drivers or builds are used. You can find an example how to enable it back if you want to in the [new documentation](https://pocketbase.io/docs/go-overview/#github-commattngo-sqlite3)._
- New hooks allowing better control over the execution chain and error handling (_including wrapping an entire hook chain in a single DB transaction_).
- Various `Record` model improvements (_support for get/set modifiers, simplfied file upload by treating the file(s) as regular field value like `record.Set("document", file)`, etc._).
- Dedicated fields structs with safer defaults to make it easier creating/updating collections programmatically.
- Option to mark field as Private/Hidden, disallowing regular users to read or modify it (_there is also a dedicated Record hook to hide/unhide Record fields programmatically from a single place_).
- Option to mark field as "Hidden", disallowing regular users to read or modify it (_there is also a dedicated Record hook to hide/unhide Record fields programmatically from a single place_).
- Option to customize the default system collection fields (`id`, `email`, `password`, etc.).
- Admins are now system `_superusers` auth records.
- Builtin rate limiter (_supports tags, wildcards and exact routes matching_).
@@ -333,23 +127,25 @@ There are many changes but to highlight some of the most notable ones:
- One-Time Password (OTP) auth method (_via email code_).
- Multi-Factor Authentication (MFA) support (_currently requires any 2 different auth methods to be used_).
- Support for Record "proxy/projection" in preparation for the planned autogeneration of typed Go record models.
- Linear OAuth2 provider ([#5909](https://github.com/pocketbase/pocketbase/pull/5909); thanks @chnfyi).
- WakaTime OAuth2 provider ([#5829](https://github.com/pocketbase/pocketbase/pull/5829); thanks @tigawanna).
- Notion OAuth2 provider ([#4999](https://github.com/pocketbase/pocketbase/pull/4999); thanks @s-li1).
- monday.com OAuth2 provider ([#5346](https://github.com/pocketbase/pocketbase/pull/5346); thanks @Jaytpa01).
- New Instagram provider compatible with the new Instagram Login APIs ([#5588](https://github.com/pocketbase/pocketbase/pull/5588); thanks @pnmcosta).
_The provider key is `instagram2` to prevent conflicts with existing linked users._
- Option to retrieve the OIDC OAuth2 user info from the `id_token` payload for the cases when the provider doesn't have a dedicated user info endpoint.
- Various minor UI improvements (_recursive `Presentable` view, slightly different collection options organization, zoom/pan for the logs chart, etc._)
- and many more...
In terms of performance, the Go standard router mux is known to be slightly slower compared to Gin, Echo, etc. implementations, but based on my local tests the difference is negliable.
The [benchmarks repo](https://github.com/pocketbase/benchmarks) will be updated with the final v0.23.0 release (_currently there seems to be ~10% memory consumption increase which I'll have to investigate to see whether it is from the router change or from the new hooks_).
#### Go/JSVM APIs changes
For upgrading to PocketBase v0.23.0, please refer to:
- Go: https://pocketbase.io/v023upgrade/go/.
- JSVM: https://pocketbase.io/v023upgrade/jsvm/.
> - Go: https://pocketbase.io/v023upgrade/go/.
> - JSVM: https://pocketbase.io/v023upgrade/jsvm/.
#### SDKs changes
- [JS SDK v0.22.0-rc](https://github.com/pocketbase/js-sdk/blob/develop/CHANGELOG.md)
- [Dart SDK v0.19.0-rc](https://github.com/pocketbase/dart-sdk/blob/develop/CHANGELOG.md)
- [JS SDK v0.22.0](https://github.com/pocketbase/js-sdk/blob/master/CHANGELOG.md)
- [Dart SDK v0.19.0](https://github.com/pocketbase/dart-sdk/blob/master/CHANGELOG.md)
#### Web APIs changes
@@ -365,6 +161,8 @@ For upgrading to PocketBase v0.23.0, please refer to:
- New `POST /api/collections/{collection}/impersonate/{id}` endpoint.
- ⚠️ If you are constructing requests to `/api/*` routes manually remove the trailing slash (_there is no longer trailing slash removal middleware registered by default_).
- ⚠️ Removed `/api/admins/*` endpoints because admins are converted to `_superusers` auth collection records.
- ⚠️ Previously when uploading new files to a multiple `file` field, new files were automatically appended to the existing field values.
+18 -1
View File
@@ -2,6 +2,23 @@
> For the most recent versions, please refer to [CHANGELOG.md](./CHANGELOG.md)
---
## v0.22.27
- Instead of unregistering the realtime clients, we now just unset their auth state on delete of the related auth record so that the clients can receive the `delete` event ([#5898](https://github.com/pocketbase/pocketbase/issues/5898)).
## v0.22.26
- (_Backported from v0.23.0-rc_) Added manual WAL checkpoints before creating the zip backup to minimize copying unnecessary data.
## v0.22.25
- Refresh the old collections state in the Import UI after successful import submission ([#5861](https://github.com/pocketbase/pocketbase/issues/5861)).
- Added randomized throttle on failed filter list requests as a very rudimentary measure since some security researches raised concern regarding the possibity of eventual side-channel attacks.
## v0.22.24
- Delete new uploaded record files in case of DB persist error ([#5845](https://github.com/pocketbase/pocketbase/issues/5845)).
@@ -1126,7 +1143,7 @@
- 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".
- Updated the logs "real" user IP to check for `Fly-Client-IP` header and changed the `X-Forwarded-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_).
+2 -9
View File
@@ -54,7 +54,6 @@ Here is a minimal example:
import (
"log"
"net/http"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
@@ -65,7 +64,7 @@ Here is a minimal example:
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
// registers new "GET /hello" route
se.Router.Get("/hello", func(re *core.RequestEvent) error {
se.Router.GET("/hello", func(re *core.RequestEvent) error {
return re.String(200, "Hello world!")
})
@@ -84,19 +83,13 @@ Here is a minimal example:
4. To build a statically linked executable, you can run `CGO_ENABLED=0 go build` and then start the created executable with `./myapp serve`.
> [!NOTE]
> PocketBase embeds SQLite, but doesn't require CGO.
>
> If CGO is enabled (aka. `CGO_ENABLED=1`), it will use [mattn/go-sqlite3](https://pkg.go.dev/github.com/mattn/go-sqlite3) driver, otherwise - [modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite).
> Enable CGO only if you really need to squeeze the read/write query performance at the expense of complicating cross compilation.
_For more details please refer to [Extend with Go](https://pocketbase.io/docs/go-overview/)._
### Building and running the repo main.go example
To build the minimal standalone executable, like the prebuilt ones in the releases page, you can simply run `go build` inside the `examples/base` directory:
0. [Install Go 1.21+](https://go.dev/doc/install) (_if you haven't already_)
0. [Install Go 1.23+](https://go.dev/doc/install) (_if you haven't already_)
1. Clone/download the repo
2. Navigate to `examples/base`
3. Run `GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build`
+5
View File
@@ -6,6 +6,11 @@ import "github.com/pocketbase/pocketbase/tools/router"
// and for consistency with the JSVM binds.
// -------------------------------------------------------------------
// ToApiError wraps err into ApiError instance (if not already).
func ToApiError(err error) *router.ApiError {
return router.ToApiError(err)
}
// NewApiError is an alias for [router.NewApiError].
func NewApiError(status int, message string, errData any) *router.ApiError {
return router.NewApiError(status, message, errData)
+6
View File
@@ -338,6 +338,12 @@ func processInternalRequest(
// apply batch request specific headers
// ---
for k, v := range ir.Headers {
// individual Authorization header keys don't have affect
// because the auth state is populated from the base event
if strings.EqualFold(k, "authorization") {
continue
}
r.Header.Set(k, v)
}
r.Header.Set("Content-Type", mw.FormDataContentType())
+3 -3
View File
@@ -294,9 +294,9 @@ func TestBatchRequest(t *testing.T) {
},
Body: strings.NewReader(`{
"requests": [
{"method":"POST", "url":"/api/collections/demo2/records", "body": {"title": "batch_create"}},
{"method":"DELETE", "url":"/api/collections/demo2/records/achvryl401bhse3"},
{"method":"PATCH", "url":"/api/collections/demo3/records/1tmknxy2868d869", "body": {"title": "batch_update"}}
{"method":"POST", "url":"/api/collections/demo2/records", "body": {"title": "batch_create"}, "headers": {"Authorization": "ignored"}},
{"method":"DELETE", "url":"/api/collections/demo2/records/achvryl401bhse3", "headers": {"Authorization": "ignored"}},
{"method":"PATCH", "url":"/api/collections/demo3/records/1tmknxy2868d869", "body": {"title": "batch_update"}, "headers": {"Authorization": "ignored"}}
]
}`),
ExpectedStatus: 200,
+1 -1
View File
@@ -724,7 +724,7 @@ func TestCollectionCreate(t *testing.T) {
ExpectedContent: []string{
`"name":"new"`,
`"type":"view"`,
`"fields":[{"autogeneratePattern":"","hidden":false,"id":"text3208210256","max":0,"min":0,"name":"id","pattern":"","presentable":false,"primaryKey":true,"required":true,"system":true,"type":"text"}]`,
`"fields":[{"autogeneratePattern":"","hidden":false,"id":"text3208210256","max":0,"min":0,"name":"id","pattern":"^[a-z0-9]+$","presentable":false,"primaryKey":true,"required":true,"system":true,"type":"text"}]`,
},
ExpectedEvents: map[string]int{
"*": 0,
+14 -2
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"log/slog"
"net/http"
"os"
"runtime"
"time"
@@ -13,6 +14,7 @@ import (
"github.com/pocketbase/pocketbase/tools/filesystem"
"github.com/pocketbase/pocketbase/tools/list"
"github.com/pocketbase/pocketbase/tools/router"
"github.com/spf13/cast"
"golang.org/x/sync/semaphore"
"golang.org/x/sync/singleflight"
)
@@ -22,10 +24,20 @@ var defaultThumbSizes = []string{"100x100"}
// bindFileApi registers the file api endpoints and the corresponding handlers.
func bindFileApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) {
maxWorkers := cast.ToInt64(os.Getenv("PB_THUMBS_MAX_WORKERS"))
if maxWorkers <= 0 {
maxWorkers = int64(runtime.NumCPU() + 2) // the value is arbitrary chosen and may change in the future
}
maxWait := cast.ToInt64(os.Getenv("PB_THUMBS_MAX_WAIT"))
if maxWait <= 0 {
maxWait = 60
}
api := fileApi{
thumbGenSem: semaphore.NewWeighted(int64(runtime.NumCPU() + 2)), // the value is arbitrary chosen and may change in the future
thumbGenPending: new(singleflight.Group),
thumbGenMaxWait: 60 * time.Second,
thumbGenSem: semaphore.NewWeighted(maxWorkers),
thumbGenMaxWait: time.Duration(maxWait) * time.Second,
}
sub := rg.Group("/files")
+1 -1
View File
@@ -18,7 +18,7 @@ const (
DefaultBodyLimitMiddlewarePriority = DefaultRateLimitMiddlewarePriority + 10
)
// BodyLimit returns a middleware function that changes the default request body size limit.
// BodyLimit returns a middleware handler that changes the default request body size limit.
//
// If limitBytes <= 0, no limit is applied.
//
+21 -5
View File
@@ -13,6 +13,7 @@ package apis
// -------------------------------------------------------------------
import (
"log"
"net/http"
"regexp"
"strconv"
@@ -124,8 +125,8 @@ var DefaultCORSConfig = CORSConfig{
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
}
// CORSWithConfig returns a CORS middleware with config.
func CORSWithConfig(config CORSConfig) *hook.Handler[*core.RequestEvent] {
// CORS returns a CORS middleware.
func CORS(config CORSConfig) *hook.Handler[*core.RequestEvent] {
// Defaults
if len(config.AllowOrigins) == 0 {
config.AllowOrigins = DefaultCORSConfig.AllowOrigins
@@ -134,13 +135,26 @@ func CORSWithConfig(config CORSConfig) *hook.Handler[*core.RequestEvent] {
config.AllowMethods = DefaultCORSConfig.AllowMethods
}
allowOriginPatterns := []string{}
allowOriginPatterns := make([]*regexp.Regexp, 0, len(config.AllowOrigins))
for _, origin := range config.AllowOrigins {
if origin == "*" {
continue // "*" is handled differently and does not need regexp
}
pattern := regexp.QuoteMeta(origin)
pattern = strings.ReplaceAll(pattern, "\\*", ".*")
pattern = strings.ReplaceAll(pattern, "\\?", ".")
pattern = "^" + pattern + "$"
allowOriginPatterns = append(allowOriginPatterns, pattern)
re, err := regexp.Compile(pattern)
if err != nil {
// This is to preserve previous behaviour - invalid patterns were just ignored.
// If we would turn this to panic, users with invalid patterns
// would have applications crashing in production due unrecovered panic.
log.Println("invalid AllowOrigins pattern", origin)
continue
}
allowOriginPatterns = append(allowOriginPatterns, re)
}
allowMethods := strings.Join(config.AllowMethods, ",")
@@ -210,7 +224,7 @@ func CORSWithConfig(config CORSConfig) *hook.Handler[*core.RequestEvent] {
}
if checkPatterns {
for _, re := range allowOriginPatterns {
if match, _ := regexp.MatchString(re, origin); match {
if match := re.MatchString(origin); match {
allowOrigin = origin
break
}
@@ -272,6 +286,7 @@ func matchSubdomain(domain, pattern string) bool {
if !matchScheme(domain, pattern) {
return false
}
didx := strings.Index(domain, "://")
pidx := strings.Index(pattern, "://")
if didx == -1 || pidx == -1 {
@@ -307,5 +322,6 @@ func matchSubdomain(domain, pattern string) bool {
return false
}
}
return false
}
+20 -17
View File
@@ -221,23 +221,26 @@ func (w *gzipResponseWriter) Push(target string, opts *http.PushOptions) error {
}
}
func (w *gzipResponseWriter) ReadFrom(r io.Reader) (n int64, err error) {
if w.wroteHeader {
w.ResponseWriter.WriteHeader(w.code)
}
rw := w.ResponseWriter
for {
switch rf := rw.(type) {
case io.ReaderFrom:
return rf.ReadFrom(r)
case router.RWUnwrapper:
rw = rf.Unwrap()
default:
return io.Copy(w.ResponseWriter, r)
}
}
}
// Note: Disable the implementation for now because in case the platform
// supports the sendfile fast-path it won't run gzipResponseWriter.Write,
// preventing compression on the fly.
//
// func (w *gzipResponseWriter) ReadFrom(r io.Reader) (n int64, err error) {
// if w.wroteHeader {
// w.ResponseWriter.WriteHeader(w.code)
// }
// rw := w.ResponseWriter
// for {
// switch rf := rw.(type) {
// case io.ReaderFrom:
// return rf.ReadFrom(r)
// case router.RWUnwrapper:
// rw = rf.Unwrap()
// default:
// return io.Copy(w.ResponseWriter, r)
// }
// }
// }
func (w *gzipResponseWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
+18 -4
View File
@@ -32,9 +32,12 @@ func rateLimit() *hook.Handler[*core.RequestEvent] {
return e.Next()
}
rule, ok := e.App.Settings().RateLimits.FindRateLimitRule(defaultRateLimitLabels(e))
rule, ok := e.App.Settings().RateLimits.FindRateLimitRule(
defaultRateLimitLabels(e),
defaultRateLimitAudience(e)...,
)
if ok {
err := checkRateLimit(e, e.Request.Pattern, rule)
err := checkRateLimit(e, rule.Label+rule.Audience, rule)
if err != nil {
return err
}
@@ -94,9 +97,9 @@ func checkCollectionRateLimit(e *core.RequestEvent, collection *core.Collection,
}
labels = append(labels, defaultRateLimitLabels(e)...)
rule, ok := e.App.Settings().RateLimits.FindRateLimitRule(labels)
rule, ok := e.App.Settings().RateLimits.FindRateLimitRule(labels, defaultRateLimitAudience(e)...)
if ok {
return checkRateLimit(e, rtId, rule)
return checkRateLimit(e, rtId+rule.Audience, rule)
}
return nil
@@ -174,6 +177,17 @@ func skipRateLimit(e *core.RequestEvent) bool {
return !e.App.Settings().RateLimits.Enabled || e.HasSuperuserAuth()
}
var defaultAuthAudience = []string{core.RateLimitRuleAudienceAll, core.RateLimitRuleAudienceAuth}
var defaultGuestAudience = []string{core.RateLimitRuleAudienceAll, core.RateLimitRuleAudienceGuest}
func defaultRateLimitAudience(e *core.RequestEvent) []string {
if e.Auth != nil {
return defaultAuthAudience
}
return defaultGuestAudience
}
func defaultRateLimitLabels(e *core.RequestEvent) []string {
return []string{e.Request.Method + " " + e.Request.URL.Path, e.Request.URL.Path}
}
+9 -9
View File
@@ -101,27 +101,27 @@ func TestDefaultRateLimitMiddleware(t *testing.T) {
{"/rate/b", 0, false, 200},
{"/rate/b", 0, false, 429},
// "auth" with guest (should be ignored)
{"/rate/auth", 0, false, 200},
{"/rate/auth", 0, false, 200},
// "auth" with guest (should fallback to the /rate/ rule)
{"/rate/auth", 0, false, 200},
{"/rate/auth", 0, false, 200},
{"/rate/auth", 0, false, 429},
{"/rate/auth", 0, false, 429},
// "auth" rule with regular user
// "auth" rule with regular user (should match the /rate/auth rule)
{"/rate/auth", 0, true, 200},
{"/rate/auth", 0, true, 429},
{"/rate/auth", 0, true, 429},
// "guest" with guest
// "guest" with guest (should match the /rate/guest rule)
{"/rate/guest", 0, false, 200},
{"/rate/guest", 0, false, 429},
{"/rate/guest", 0, false, 429},
// "guest" rule with regular user (should be ignored)
{"/rate/guest", 0, true, 200},
{"/rate/guest", 0, true, 200},
{"/rate/guest", 0, true, 200},
// "guest" rule with regular user (should fallback to the /rate/ rule)
{"/rate/guest", 1, true, 200},
{"/rate/guest", 0, true, 200},
{"/rate/guest", 0, true, 429},
{"/rate/guest", 0, true, 429},
}
for _, s := range scenarios {
+4 -4
View File
@@ -242,8 +242,8 @@ func realtimeUpdateClientsAuth(app core.App, newAuthRecord *core.Record) error {
return group.Wait()
}
// unregisterClientsByAuthModel unregister all clients that has the provided auth model.
func realtimeUnregisterClientsByAuth(app core.App, authModel core.Model) error {
// realtimeUnsetClientsAuthState unsets the auth state of all clients that have the provided auth model.
func realtimeUnsetClientsAuthState(app core.App, authModel core.Model) error {
chunks := app.SubscriptionsBroker().ChunkedClients(clientsChunkSize)
group := new(errgroup.Group)
@@ -255,7 +255,7 @@ func realtimeUnregisterClientsByAuth(app core.App, authModel core.Model) error {
if clientAuth != nil &&
clientAuth.Id == authModel.PK() &&
clientAuth.Collection().Name == authModel.TableName() {
app.SubscriptionsBroker().Unregister(client.Id())
client.Unset(RealtimeClientAuthKey)
}
}
@@ -293,7 +293,7 @@ func bindRealtimeEvents(app core.App) {
Func: func(e *core.ModelEvent) error {
collection := realtimeResolveRecordCollection(e.App, e.Model)
if collection != nil && collection.IsAuth() {
if err := realtimeUnregisterClientsByAuth(e.App, e.Model); err != nil {
if err := realtimeUnsetClientsAuthState(e.App, e.Model); err != nil {
app.Logger().Warn(
"Failed to remove client(s) associated to the deleted auth model",
slog.Any("id", e.Model.PK()),
+64 -14
View File
@@ -427,26 +427,51 @@ func TestRealtimeAuthRecordDeleteEvent(t *testing.T) {
// init realtime handlers
apis.NewRouter(testApp)
authRecord, err := testApp.FindAuthRecordByEmail("users", "test@example.com")
authRecord1, err := testApp.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
client := subscriptions.NewDefaultClient()
client.Set(apis.RealtimeClientAuthKey, authRecord)
testApp.SubscriptionsBroker().Register(client)
authRecord2, err := testApp.FindAuthRecordByEmail("users", "test2@example.com")
if err != nil {
t.Fatal(err)
}
client1 := subscriptions.NewDefaultClient()
client1.Set(apis.RealtimeClientAuthKey, authRecord1)
testApp.SubscriptionsBroker().Register(client1)
client2 := subscriptions.NewDefaultClient()
client2.Set(apis.RealtimeClientAuthKey, authRecord1)
testApp.SubscriptionsBroker().Register(client2)
client3 := subscriptions.NewDefaultClient()
client3.Set(apis.RealtimeClientAuthKey, authRecord2)
testApp.SubscriptionsBroker().Register(client3)
// mock delete event
e := new(core.ModelEvent)
e.App = testApp
e.Type = core.ModelEventTypeDelete
e.Context = context.Background()
e.Model = authRecord
e.Model = authRecord1
testApp.OnModelAfterDeleteSuccess().Trigger(e)
if total := len(testApp.SubscriptionsBroker().Clients()); total != 0 {
t.Fatalf("Expected no subscription clients, found %d", total)
if total := len(testApp.SubscriptionsBroker().Clients()); total != 3 {
t.Fatalf("Expected %d subscription clients, found %d", 3, total)
}
if auth := client1.Get(apis.RealtimeClientAuthKey); auth != nil {
t.Fatalf("[client1] Expected the auth state to be unset, found %#v", auth)
}
if auth := client2.Get(apis.RealtimeClientAuthKey); auth != nil {
t.Fatalf("[client2] Expected the auth state to be unset, found %#v", auth)
}
if auth := client3.Get(apis.RealtimeClientAuthKey); auth == nil || auth.(*core.Record).Id != authRecord2.Id {
t.Fatalf("[client3] Expected the auth state to be left unchanged, found %#v", auth)
}
}
@@ -524,17 +549,30 @@ func TestRealtimeCustomAuthModelDeleteEvent(t *testing.T) {
// init realtime handlers
apis.NewRouter(testApp)
authRecord, err := testApp.FindAuthRecordByEmail("users", "test@example.com")
authRecord1, err := testApp.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
client := subscriptions.NewDefaultClient()
client.Set(apis.RealtimeClientAuthKey, authRecord)
testApp.SubscriptionsBroker().Register(client)
authRecord2, err := testApp.FindAuthRecordByEmail("users", "test2@example.com")
if err != nil {
t.Fatal(err)
}
client1 := subscriptions.NewDefaultClient()
client1.Set(apis.RealtimeClientAuthKey, authRecord1)
testApp.SubscriptionsBroker().Register(client1)
client2 := subscriptions.NewDefaultClient()
client2.Set(apis.RealtimeClientAuthKey, authRecord1)
testApp.SubscriptionsBroker().Register(client2)
client3 := subscriptions.NewDefaultClient()
client3.Set(apis.RealtimeClientAuthKey, authRecord2)
testApp.SubscriptionsBroker().Register(client3)
// refetch the authRecord as CustomUser
customUser, err := findCustomUserByEmail(testApp, "test@example.com")
customUser, err := findCustomUserByEmail(testApp, authRecord1.Email())
if err != nil {
t.Fatal(err)
}
@@ -544,8 +582,20 @@ func TestRealtimeCustomAuthModelDeleteEvent(t *testing.T) {
t.Fatal(err)
}
if total := len(testApp.SubscriptionsBroker().Clients()); total != 0 {
t.Fatalf("Expected no subscription clients, found %d", total)
if total := len(testApp.SubscriptionsBroker().Clients()); total != 3 {
t.Fatalf("Expected %d subscription clients, found %d", 3, total)
}
if auth := client1.Get(apis.RealtimeClientAuthKey); auth != nil {
t.Fatalf("[client1] Expected the auth state to be unset, found %#v", auth)
}
if auth := client2.Get(apis.RealtimeClientAuthKey); auth != nil {
t.Fatalf("[client2] Expected the auth state to be unset, found %#v", auth)
}
if auth := client3.Get(apis.RealtimeClientAuthKey); auth == nil || auth.(*core.Record).Id != authRecord2.Id {
t.Fatalf("[client3] Expected the auth state to be left unchanged, found %#v", auth)
}
}
+2 -2
View File
@@ -49,12 +49,12 @@ func recordRequestOTP(e *core.RequestEvent) error {
return e.App.OnRecordRequestOTPRequest().Trigger(event, func(e *core.RecordCreateOTPRequestEvent) error {
if e.Record == nil {
// write a dummy 200 response as a very rudimentary user emails enumeration protection
// write a dummy 200 response as a very rudimentary emails enumeration "protection"
e.JSON(http.StatusOK, map[string]string{
"otpId": core.GenerateDefaultRandomId(),
})
return fmt.Errorf("failed to fetch %s record with email %s: %w", collection.Name, form.Email, err)
return fmt.Errorf("missing or invalid %s OTP auth record with email %s", collection.Name, form.Email)
}
var otp *core.OTP
+12 -4
View File
@@ -2,6 +2,7 @@ package apis
import (
"context"
"database/sql"
"errors"
"fmt"
"log/slog"
@@ -95,6 +96,10 @@ func recordAuthWithOAuth2(e *core.RequestEvent) error {
"provider": form.Provider,
"providerId": authUser.Id,
})
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return e.InternalServerError("Failed OAuth2 relation check.", err)
}
switch {
case err == nil && externalAuthRel != nil:
authRecord, err = e.App.FindRecordById(form.collection, externalAuthRel.RecordRef())
@@ -106,7 +111,10 @@ func recordAuthWithOAuth2(e *core.RequestEvent) error {
authRecord = fallbackAuthRecord
case authUser.Email != "":
// look for an existing auth record by the external auth record's email
authRecord, _ = e.App.FindAuthRecordByEmail(form.collection.Id, authUser.Email)
authRecord, err = e.App.FindAuthRecordByEmail(form.collection.Id, authUser.Email)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return e.InternalServerError("Failed OAuth2 auth record check.", err)
}
}
// ---------------------------------------------------------------
@@ -167,7 +175,7 @@ type recordOAuth2LoginForm struct {
func (form *recordOAuth2LoginForm) validate() error {
return validation.ValidateStruct(form,
validation.Field(&form.Provider, validation.Required, validation.By(form.checkProviderName)),
validation.Field(&form.Provider, validation.Required, validation.Length(0, 100), validation.By(form.checkProviderName)),
validation.Field(&form.Code, validation.Required),
validation.Field(&form.RedirectURL, validation.Required),
)
@@ -178,7 +186,7 @@ func (form *recordOAuth2LoginForm) checkProviderName(value any) error {
_, ok := form.collection.OAuth2.GetProviderConfig(name)
if !ok {
return validation.NewError("validation_invalid_provider", fmt.Sprintf("Provider with name %q is missing or is not enabled.", name)).
return validation.NewError("validation_invalid_provider", "Provider with name {{.name}} is missing or is not enabled.").
SetParams(map[string]any{"name": name})
}
@@ -237,7 +245,7 @@ func oauth2Submit(e *core.RecordAuthWithOAuth2RequestEvent, optExternalAuth *cor
payload[e.Collection.OAuth2.MappedFields.Username] = e.OAuth2User.Username
}
if _, ok := payload[e.Collection.OAuth2.MappedFields.AvatarURL]; !ok &&
// no existing OAuth2 mapping
// no explicit avatar payload value and existing OAuth2 mapping
e.Collection.OAuth2.MappedFields.AvatarURL != "" &&
// non-empty OAuth2 avatar url
e.OAuth2User.AvatarURL != "" {
+36 -14
View File
@@ -56,18 +56,22 @@ func TestRecordAuthWithOAuth2Redirect(t *testing.T) {
}
}
// note: don't exit because it is usually called as part of a separate goroutine
checkClientMessages := func(t testing.TB, clientId string, msg subscriptions.Message, expectedMessages map[string][]string) {
if len(expectedMessages[clientId]) == 0 {
t.Fatalf("Unexpected client %q message, got %s:\n%s", clientId, msg.Name, msg.Data)
t.Errorf("Unexpected client %q message, got %q:\n%q", clientId, msg.Name, msg.Data)
return
}
if msg.Name != "@oauth2" {
t.Fatalf("Expected @oauth2 msg.Name, got %q", msg.Name)
t.Errorf("Expected @oauth2 msg.Name, got %q", msg.Name)
return
}
for _, txt := range expectedMessages[clientId] {
if !strings.Contains(string(msg.Data), txt) {
t.Fatalf("Failed to find %q in \n%s", txt, msg.Data)
t.Errorf("Failed to find %q in \n%s", txt, msg.Data)
return
}
}
}
@@ -91,19 +95,37 @@ func TestRecordAuthWithOAuth2Redirect(t *testing.T) {
for {
select {
case msg := <-clients["c1"].Channel():
checkClientMessages(t, "c1", msg, expectedMessages)
case msg := <-clients["c2"].Channel():
checkClientMessages(t, "c2", msg, expectedMessages)
case msg := <-clients["c3"].Channel():
checkClientMessages(t, "c3", msg, expectedMessages)
case msg := <-clients["c4"].Channel():
checkClientMessages(t, "c4", msg, expectedMessages)
case msg := <-clients["c5"].Channel():
checkClientMessages(t, "c5", msg, expectedMessages)
case msg, ok := <-clients["c1"].Channel():
if ok {
checkClientMessages(t, "c1", msg, expectedMessages)
} else {
t.Errorf("Unexpected c1 closed channel")
}
case msg, ok := <-clients["c2"].Channel():
if ok {
checkClientMessages(t, "c2", msg, expectedMessages)
} else {
t.Errorf("Unexpected c2 closed channel")
}
case msg, ok := <-clients["c3"].Channel():
if ok {
checkClientMessages(t, "c3", msg, expectedMessages)
} else {
t.Errorf("Unexpected c3 closed channel")
}
case msg, ok := <-clients["c4"].Channel():
if ok {
checkClientMessages(t, "c4", msg, expectedMessages)
} else {
t.Errorf("Unexpected c4 closed channel")
}
case _, ok := <-clients["c5"].Channel():
if ok {
t.Errorf("Expected c5 channel to be closed")
}
case <-ctx.Done():
for _, c := range clients {
close(c.Channel())
c.Discard()
}
return
}
+5 -5
View File
@@ -79,17 +79,17 @@ func recordAuthWithOTP(e *core.RequestEvent) error {
}
}
err = RecordAuthResponse(e.RequestEvent, e.Record, core.MFAMethodOTP, nil)
if err != nil {
return err
}
// try to delete the used otp
err = e.App.Delete(e.OTP)
if err != nil {
e.App.Logger().Error("Failed to delete used OTP", "error", err, "otpId", e.OTP.Id)
}
err = RecordAuthResponse(e.RequestEvent, e.Record, core.MFAMethodOTP, nil)
if err != nil {
return err
}
return nil
})
}
+22 -2
View File
@@ -13,6 +13,7 @@ import (
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/forms"
"github.com/pocketbase/pocketbase/tools/filesystem"
"github.com/pocketbase/pocketbase/tools/list"
"github.com/pocketbase/pocketbase/tools/router"
"github.com/pocketbase/pocketbase/tools/search"
)
@@ -561,9 +562,28 @@ func recordDataFromRequest(e *core.RequestEvent, record *core.Record) (map[strin
return nil, err
}
if len(uploadedFiles) > 0 {
for k, v := range uploadedFiles {
result[k] = v
for k, files := range uploadedFiles {
uploaded := make([]any, 0, len(files))
// if not remove/prepend/append -> merge with the submitted
// info.Body values to prevent accidental old files deletion
if info.Body[k] != nil &&
!strings.HasPrefix(k, "+") &&
!strings.HasSuffix(k, "+") &&
!strings.HasSuffix(k, "-") {
existing := list.ToUniqueStringSlice(info.Body[k])
for _, name := range existing {
uploaded = append(uploaded, name)
}
}
for _, file := range files {
uploaded = append(uploaded, file)
}
result[k] = uploaded
}
result = record.ReplaceModifiers(result)
}
+66 -1
View File
@@ -259,6 +259,69 @@ func TestRecordCrudList(t *testing.T) {
"OnRecordEnrich": 4,
},
},
{
Name: "authenticated regular record filtering with a hidden field",
Method: http.MethodGet,
URL: "/api/collections/demo3/records?filter=title~'test'",
Headers: map[string]string{
// clients, test@example.com
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
col, err := app.FindCollectionByNameOrId("demo3")
if err != nil {
t.Fatal(err)
}
// mock hidden field
col.Fields.GetByName("title").SetHidden(true)
if err = app.Save(col); err != nil {
t.Fatal(err)
}
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "superuser filtering with a hidden field",
Method: http.MethodGet,
URL: "/api/collections/demo3/records?filter=title~'test'",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
col, err := app.FindCollectionByNameOrId("demo3")
if err != nil {
t.Fatal(err)
}
// mock hidden field
col.Fields.GetByName("title").SetHidden(true)
if err = app.Save(col); err != nil {
t.Fatal(err)
}
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":4`,
`"items":[{`,
`"id":"1tmknxy2868d869"`,
`"id":"lcl9d87w22ml6jy"`,
`"id":"7nwo8tuiatetxdm"`,
`"id":"mk5fmymtx4wsprk"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 4,
},
},
{
Name: ":rule modifer",
Method: http.MethodGet,
@@ -2345,7 +2408,7 @@ func TestRecordCrudUpdate(t *testing.T) {
}
formData3, mp3, err3 := tests.MockMultipartData(map[string]string{
router.JSONPayloadKey: `{"title": "title_test3", "testPayload": 123}`,
router.JSONPayloadKey: `{"title": "title_test3", "testPayload": 123, "files":"300_JdfBOieXAW.png"}`,
}, "files")
if err3 != nil {
t.Fatal(err3)
@@ -2686,6 +2749,8 @@ func TestRecordCrudUpdate(t *testing.T) {
`"id":"mk5fmymtx4wsprk"`,
`"title":"title_test3"`,
`"files":["`,
`"300_JdfBOieXAW.png"`,
`"tmpfile_`,
},
ExpectedEvents: map[string]int{
"*": 0,
+14 -5
View File
@@ -20,6 +20,8 @@ const (
fieldsQueryParam = "fields"
)
var ErrMFA = errors.New("mfa required")
// RecordAuthResponse writes standardized json record auth response
// into the specified request context.
//
@@ -70,9 +72,12 @@ func recordAuthResponse(e *core.RequestEvent, authRecord *core.Record, token str
// require additional authentication
if mfaId != "" {
return e.JSON(http.StatusUnauthorized, map[string]string{
// eagerly write the mfa response and return an err so that
// external middlewars are aware that the auth response requires an extra step
e.JSON(http.StatusUnauthorized, map[string]string{
"mfaId": mfaId,
})
return ErrMFA
}
// ---
@@ -109,13 +114,17 @@ func recordAuthResponse(e *core.RequestEvent, authRecord *core.Record, token str
}
}
result := map[string]any{
"token": e.Token,
"record": e.Record,
result := struct {
Meta any `json:"meta,omitempty"`
Record *core.Record `json:"record"`
Token string `json:"token"`
}{
Token: e.Token,
Record: e.Record,
}
if e.Meta != nil {
result["meta"] = e.Meta
result.Meta = e.Meta
}
return e.JSON(http.StatusOK, result)
+5 -4
View File
@@ -2,6 +2,7 @@ package apis_test
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
@@ -584,8 +585,8 @@ func TestRecordAuthResponseMFACheck(t *testing.T) {
user.Collection().MFA.Rule = "1=1"
err = apis.RecordAuthResponse(event, user, "example", nil)
if err != nil {
t.Fatalf("Expected nil, got error: %v", err)
if !errors.Is(err, apis.ErrMFA) {
t.Fatalf("Expected ErrMFA, got: %v", err)
}
body := rec.Body.String()
@@ -602,8 +603,8 @@ func TestRecordAuthResponseMFACheck(t *testing.T) {
resetMFAs(user)
err = apis.RecordAuthResponse(event, user, "example", nil)
if err != nil {
t.Fatalf("Expected nil, got error: %v", err)
if !errors.Is(err, apis.ErrMFA) {
t.Fatalf("Expected ErrMFA, got: %v", err)
}
body := rec.Body.String()
+8 -2
View File
@@ -72,17 +72,23 @@ func Serve(app core.App, config ServeConfig) error {
return err
}
pbRouter.Bind(CORSWithConfig(CORSConfig{
pbRouter.Bind(CORS(CORSConfig{
AllowOrigins: config.AllowedOrigins,
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
}))
pbRouter.GET("/_/{path...}", Static(ui.DistDirFS, false)).
BindFunc(func(e *core.RequestEvent) error {
// ingore root path
// ignore root path
if e.Request.PathValue(StaticWildcardParam) != "" {
e.Response.Header().Set("Cache-Control", "max-age=1209600, stale-while-revalidate=86400")
}
// add a default CSP
if e.Response.Header().Get("Content-Security-Policy") == "" {
e.Response.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' http://127.0.0.1:* data: blob:; connect-src 'self' http://127.0.0.1:*; script-src 'self' 'sha256-GRUzBA7PzKYug7pqxv5rJaec5bwDCw1Vo6/IXwvD3Tc='")
}
return e.Next()
}).
Bind(Gzip())
+14 -6
View File
@@ -340,8 +340,6 @@ func (app *BaseApp) initHooks() {
// NB! Note that using the returned app instance may cause data integrity errors
// since the Record validations and data normalizations (including files uploads)
// rely on the app hooks to work.
//
// @todo consider caching the created instance?
func (app *BaseApp) UnsafeWithoutHooks() App {
clone := *app
@@ -1140,8 +1138,8 @@ var sqlLogReplacements = map[string]string{
"]]": "`",
"<nil>": "NULL",
}
var sqlLogPrefixedTableIdentifierPattern = regexp.MustCompile(`\[\[(.+)\.(.+)\]\]`)
var sqlLogPrefixedColumnIdentifierPattern = regexp.MustCompile(`\{\{(.+)\.(.+)\}\}`)
var sqlLogPrefixedTableIdentifierPattern = regexp.MustCompile(`\[\[([^\[\]\{\}\.]+)\.([^\[\]\{\}\.]+)\]\]`)
var sqlLogPrefixedColumnIdentifierPattern = regexp.MustCompile(`\{\{([^\[\]\{\}\.]+)\.([^\[\]\{\}\.]+)\}\}`)
// normalizeSQLLog replaces common query builder charactes with their plain SQL version for easier debugging.
// The query is still not suitable for execution and should be used only for log and debug purposes
@@ -1238,8 +1236,18 @@ func (app *BaseApp) registerBaseHooks() {
Priority: 999,
})
app.Cron().Add("__pbPragmaOptimize__", "0 0 * * *", func() {
_, execErr := app.DB().NewQuery("PRAGMA optimize").Execute()
app.Cron().Add("__pbDBOptimize__", "0 0 * * *", func() {
_, execErr := app.NonconcurrentDB().NewQuery("PRAGMA wal_checkpoint(TRUNCATE)").Execute()
if execErr != nil {
app.Logger().Warn("Failed to run periodic PRAGMA wal_checkpoint for the main DB", slog.String("error", execErr.Error()))
}
_, execErr = app.AuxNonconcurrentDB().NewQuery("PRAGMA wal_checkpoint(TRUNCATE)").Execute()
if execErr != nil {
app.Logger().Warn("Failed to run periodic PRAGMA wal_checkpoint for the auxiliary DB", slog.String("error", execErr.Error()))
}
_, execErr = app.DB().NewQuery("PRAGMA optimize").Execute()
if execErr != nil {
app.Logger().Warn("Failed to run periodic PRAGMA optimize", slog.String("error", execErr.Error()))
}
+5
View File
@@ -76,6 +76,11 @@ func (app *BaseApp) CreateBackup(ctx context.Context, name string) error {
tempPath := filepath.Join(localTempDir, "pb_backup_"+security.PseudorandomString(6))
createErr := e.App.RunInTransaction(func(txApp App) error {
return txApp.AuxRunInTransaction(func(txApp App) error {
// run manual checkpoint and truncate the WAL files
// (errors are ignored because it is not that important and the PRAGMA may not be supported by the used driver)
txApp.DB().NewQuery("PRAGMA wal_checkpoint(TRUNCATE)").Execute()
txApp.AuxDB().NewQuery("PRAGMA wal_checkpoint(TRUNCATE)").Execute()
return archive.Create(txApp.DataDir(), tempPath, e.Exclude...)
})
})
+6
View File
@@ -86,6 +86,12 @@ func (app *BaseApp) ImportCollections(toImport []map[string]any, deleteMissing b
continue
}
if imported.Fields.GetById(f.GetId()) == nil {
// replace with the existing id to prevent accidental column deletion
// since otherwise the imported field will be treated as a new one
found := imported.Fields.GetByName(f.GetName())
if found != nil && found.Type() == f.Type() {
found.SetId(f.GetId())
}
imported.Fields.Add(f)
}
}
+58 -14
View File
@@ -28,15 +28,20 @@ const (
const systemHookIdCollection = "__pbCollectionSystemHook__"
const defaultLowercaseRecordIdPattern = "^[a-z0-9]+$"
func (app *BaseApp) registerCollectionHooks() {
app.OnModelValidate().Bind(&hook.Handler[*ModelEvent]{
Id: systemHookIdCollection,
Func: func(me *ModelEvent) error {
if ce, ok := newCollectionEventFromModelEvent(me); ok {
return me.App.OnCollectionValidate().Trigger(ce, func(ce *CollectionEvent) error {
err := me.App.OnCollectionValidate().Trigger(ce, func(ce *CollectionEvent) error {
syncModelEventWithCollectionEvent(me, ce)
defer syncCollectionEventWithModelEvent(ce, me)
return me.Next()
})
syncModelEventWithCollectionEvent(me, ce)
return err
}
return me.Next()
@@ -48,10 +53,13 @@ func (app *BaseApp) registerCollectionHooks() {
Id: systemHookIdCollection,
Func: func(me *ModelEvent) error {
if ce, ok := newCollectionEventFromModelEvent(me); ok {
return me.App.OnCollectionCreate().Trigger(ce, func(ce *CollectionEvent) error {
err := me.App.OnCollectionCreate().Trigger(ce, func(ce *CollectionEvent) error {
syncModelEventWithCollectionEvent(me, ce)
defer syncCollectionEventWithModelEvent(ce, me)
return me.Next()
})
syncModelEventWithCollectionEvent(me, ce)
return err
}
return me.Next()
@@ -63,10 +71,13 @@ func (app *BaseApp) registerCollectionHooks() {
Id: systemHookIdCollection,
Func: func(me *ModelEvent) error {
if ce, ok := newCollectionEventFromModelEvent(me); ok {
return me.App.OnCollectionCreateExecute().Trigger(ce, func(ce *CollectionEvent) error {
err := me.App.OnCollectionCreateExecute().Trigger(ce, func(ce *CollectionEvent) error {
syncModelEventWithCollectionEvent(me, ce)
defer syncCollectionEventWithModelEvent(ce, me)
return me.Next()
})
syncModelEventWithCollectionEvent(me, ce)
return err
}
return me.Next()
@@ -78,10 +89,13 @@ func (app *BaseApp) registerCollectionHooks() {
Id: systemHookIdCollection,
Func: func(me *ModelEvent) error {
if ce, ok := newCollectionEventFromModelEvent(me); ok {
return me.App.OnCollectionAfterCreateSuccess().Trigger(ce, func(ce *CollectionEvent) error {
err := me.App.OnCollectionAfterCreateSuccess().Trigger(ce, func(ce *CollectionEvent) error {
syncModelEventWithCollectionEvent(me, ce)
defer syncCollectionEventWithModelEvent(ce, me)
return me.Next()
})
syncModelEventWithCollectionEvent(me, ce)
return err
}
return me.Next()
@@ -93,10 +107,13 @@ func (app *BaseApp) registerCollectionHooks() {
Id: systemHookIdCollection,
Func: func(me *ModelErrorEvent) error {
if ce, ok := newCollectionErrorEventFromModelErrorEvent(me); ok {
return me.App.OnCollectionAfterCreateError().Trigger(ce, func(ce *CollectionErrorEvent) error {
err := me.App.OnCollectionAfterCreateError().Trigger(ce, func(ce *CollectionErrorEvent) error {
syncModelErrorEventWithCollectionErrorEvent(me, ce)
defer syncCollectionErrorEventWithModelErrorEvent(ce, me)
return me.Next()
})
syncModelErrorEventWithCollectionErrorEvent(me, ce)
return err
}
return me.Next()
@@ -108,10 +125,13 @@ func (app *BaseApp) registerCollectionHooks() {
Id: systemHookIdCollection,
Func: func(me *ModelEvent) error {
if ce, ok := newCollectionEventFromModelEvent(me); ok {
return me.App.OnCollectionUpdate().Trigger(ce, func(ce *CollectionEvent) error {
err := me.App.OnCollectionUpdate().Trigger(ce, func(ce *CollectionEvent) error {
syncModelEventWithCollectionEvent(me, ce)
defer syncCollectionEventWithModelEvent(ce, me)
return me.Next()
})
syncModelEventWithCollectionEvent(me, ce)
return err
}
return me.Next()
@@ -123,10 +143,13 @@ func (app *BaseApp) registerCollectionHooks() {
Id: systemHookIdCollection,
Func: func(me *ModelEvent) error {
if ce, ok := newCollectionEventFromModelEvent(me); ok {
return me.App.OnCollectionUpdateExecute().Trigger(ce, func(ce *CollectionEvent) error {
err := me.App.OnCollectionUpdateExecute().Trigger(ce, func(ce *CollectionEvent) error {
syncModelEventWithCollectionEvent(me, ce)
defer syncCollectionEventWithModelEvent(ce, me)
return me.Next()
})
syncModelEventWithCollectionEvent(me, ce)
return err
}
return me.Next()
@@ -138,10 +161,13 @@ func (app *BaseApp) registerCollectionHooks() {
Id: systemHookIdCollection,
Func: func(me *ModelEvent) error {
if ce, ok := newCollectionEventFromModelEvent(me); ok {
return me.App.OnCollectionAfterUpdateSuccess().Trigger(ce, func(ce *CollectionEvent) error {
err := me.App.OnCollectionAfterUpdateSuccess().Trigger(ce, func(ce *CollectionEvent) error {
syncModelEventWithCollectionEvent(me, ce)
defer syncCollectionEventWithModelEvent(ce, me)
return me.Next()
})
syncModelEventWithCollectionEvent(me, ce)
return err
}
return me.Next()
@@ -153,10 +179,13 @@ func (app *BaseApp) registerCollectionHooks() {
Id: systemHookIdCollection,
Func: func(me *ModelErrorEvent) error {
if ce, ok := newCollectionErrorEventFromModelErrorEvent(me); ok {
return me.App.OnCollectionAfterUpdateError().Trigger(ce, func(ce *CollectionErrorEvent) error {
err := me.App.OnCollectionAfterUpdateError().Trigger(ce, func(ce *CollectionErrorEvent) error {
syncModelErrorEventWithCollectionErrorEvent(me, ce)
defer syncCollectionErrorEventWithModelErrorEvent(ce, me)
return me.Next()
})
syncModelErrorEventWithCollectionErrorEvent(me, ce)
return err
}
return me.Next()
@@ -168,10 +197,13 @@ func (app *BaseApp) registerCollectionHooks() {
Id: systemHookIdCollection,
Func: func(me *ModelEvent) error {
if ce, ok := newCollectionEventFromModelEvent(me); ok {
return me.App.OnCollectionDelete().Trigger(ce, func(ce *CollectionEvent) error {
err := me.App.OnCollectionDelete().Trigger(ce, func(ce *CollectionEvent) error {
syncModelEventWithCollectionEvent(me, ce)
defer syncCollectionEventWithModelEvent(ce, me)
return me.Next()
})
syncModelEventWithCollectionEvent(me, ce)
return err
}
return me.Next()
@@ -183,10 +215,13 @@ func (app *BaseApp) registerCollectionHooks() {
Id: systemHookIdCollection,
Func: func(me *ModelEvent) error {
if ce, ok := newCollectionEventFromModelEvent(me); ok {
return me.App.OnCollectionDeleteExecute().Trigger(ce, func(ce *CollectionEvent) error {
err := me.App.OnCollectionDeleteExecute().Trigger(ce, func(ce *CollectionEvent) error {
syncModelEventWithCollectionEvent(me, ce)
defer syncCollectionEventWithModelEvent(ce, me)
return me.Next()
})
syncModelEventWithCollectionEvent(me, ce)
return err
}
return me.Next()
@@ -198,10 +233,13 @@ func (app *BaseApp) registerCollectionHooks() {
Id: systemHookIdCollection,
Func: func(me *ModelEvent) error {
if ce, ok := newCollectionEventFromModelEvent(me); ok {
return me.App.OnCollectionAfterDeleteSuccess().Trigger(ce, func(ce *CollectionEvent) error {
err := me.App.OnCollectionAfterDeleteSuccess().Trigger(ce, func(ce *CollectionEvent) error {
syncModelEventWithCollectionEvent(me, ce)
defer syncCollectionEventWithModelEvent(ce, me)
return me.Next()
})
syncModelEventWithCollectionEvent(me, ce)
return err
}
return me.Next()
@@ -213,10 +251,13 @@ func (app *BaseApp) registerCollectionHooks() {
Id: systemHookIdCollection,
Func: func(me *ModelErrorEvent) error {
if ce, ok := newCollectionErrorEventFromModelErrorEvent(me); ok {
return me.App.OnCollectionAfterDeleteError().Trigger(ce, func(ce *CollectionErrorEvent) error {
err := me.App.OnCollectionAfterDeleteError().Trigger(ce, func(ce *CollectionErrorEvent) error {
syncModelErrorEventWithCollectionErrorEvent(me, ce)
defer syncCollectionErrorEventWithModelErrorEvent(ce, me)
return me.Next()
})
syncModelErrorEventWithCollectionErrorEvent(me, ce)
return err
}
return me.Next()
@@ -890,7 +931,7 @@ func (c *Collection) initIdField() {
Required: true,
Min: 15,
Max: 15,
Pattern: `^[a-z0-9]+$`,
Pattern: defaultLowercaseRecordIdPattern,
AutogeneratePattern: `[a-z0-9]{15}`,
}
@@ -902,6 +943,9 @@ func (c *Collection) initIdField() {
field.Required = true
field.PrimaryKey = true
field.Hidden = false
if field.Pattern == "" {
field.Pattern = defaultLowercaseRecordIdPattern
}
}
}
+2 -2
View File
@@ -445,7 +445,7 @@ func checkForDuplicatedProviders(value any) error {
if _, ok := existing[c.Name]; ok {
return validation.Errors{
strconv.Itoa(i): validation.Errors{
"name": validation.NewError("validation_duplicated_provider", "The provider "+c.Name+" is already registered.").
"name": validation.NewError("validation_duplicated_provider", "The provider {{.name}} is already registered.").
SetParams(map[string]any{"name": c.Name}),
},
}
@@ -493,7 +493,7 @@ func checkProviderName(value any) error {
}
if _, err := auth.NewProviderByName(name); err != nil {
return validation.NewError("validation_missing_provider", "Invalid or missing provider with name "+name+".").
return validation.NewError("validation_missing_provider", "Invalid or missing provider with name {{.name}}.").
SetParams(map[string]any{"name": name})
}
+4 -4
View File
@@ -75,16 +75,16 @@ func (app *BaseApp) FindCollectionByNameOrId(nameOrId string) (*Collection, erro
return m, nil
}
// FindCachedCollectionByNameOrId is similar to [App.FindCollectionByNameOrId]
// FindCachedCollectionByNameOrId is similar to [BaseApp.FindCollectionByNameOrId]
// but retrieves the Collection from the app cache instead of making a db call.
//
// NB! This method is suitable for read-only Collection operations.
//
// Returns [sql.ErrNoRows] if no Collection is found for consistency
// with the [App.FindCollectionByNameOrId] method.
// with the [BaseApp.FindCollectionByNameOrId] method.
//
// If you plan making changes to the returned Collection model,
// use [App.FindCollectionByNameOrId] instead.
// use [BaseApp.FindCollectionByNameOrId] instead.
//
// Caveats:
//
@@ -94,7 +94,7 @@ func (app *BaseApp) FindCollectionByNameOrId(nameOrId string) (*Collection, erro
// - If you are updating a Collection in a transaction and then call this method before commit,
// it'll return the cached Collection state and not the one from the uncommitted transaction.
// - The cache is automatically updated on collections db change (create/update/delete).
// To manually reload the cache you can call [App.ReloadCachedCollections()]
// To manually reload the cache you can call [BaseApp.ReloadCachedCollections]
func (app *BaseApp) FindCachedCollectionByNameOrId(nameOrId string) (*Collection, error) {
collections, _ := app.Store().Get(StoreKeyCachedCollections).([]*Collection)
if collections == nil {
+1 -3
View File
@@ -349,9 +349,7 @@ func createCollectionIndexes(app App, collection *Collection) error {
errs[strconv.Itoa(i)] = validation.NewError(
"validation_invalid_index_expression",
fmt.Sprintf("Failed to create index %s - %v.", parsed.IndexName, err.Error()),
).SetParams(map[string]any{
"indexName": parsed.IndexName,
})
)
continue
}
}
+24 -8
View File
@@ -268,7 +268,7 @@ func (validator *collectionValidator) checkFieldDuplicates(value any) error {
strconv.Itoa(i): validation.Errors{
"name": validation.NewError(
"validation_duplicated_field_name",
fmt.Sprintf("Duplicated or invalid field name %q", field.GetName()),
"Duplicated or invalid field name {{.fieldName}}",
).SetParams(map[string]any{
"fieldName": field.GetName(),
}),
@@ -452,12 +452,12 @@ func (cv *collectionValidator) checkFieldsForUniqueIndex(value any) error {
for _, name := range names {
field := cv.new.Fields.GetByName(name)
if field == nil {
return validation.NewError("validation_missing_field", fmt.Sprintf("Invalid or missing field %q", name)).
return validation.NewError("validation_missing_field", "Invalid or missing field {{.fieldName}}").
SetParams(map[string]any{"fieldName": name})
}
if !dbutils.HasSingleColumnUniqueIndex(name, cv.new.Indexes) {
return validation.NewError("validation_missing_unique_constraint", fmt.Sprintf("The field %q doesn't have a UNIQUE constraint.", name)).
return validation.NewError("validation_missing_unique_constraint", "The field {{.fieldName}} doesn't have a UNIQUE constraint.").
SetParams(map[string]any{"fieldName": name})
}
}
@@ -566,7 +566,7 @@ func (cv *collectionValidator) checkIndexes(value any) error {
return validation.Errors{
strconv.Itoa(i): validation.NewError(
"validation_existing_index_name",
"The index name is already used in "+usedTblName+" collection.",
"The index name is already used in {{.usedTableName}} collection.",
).SetParams(map[string]any{"usedTableName": usedTblName}),
}
}
@@ -608,6 +608,12 @@ func (cv *collectionValidator) checkIndexes(value any) error {
continue
}
// reset collate and sort since they are not important for the unique constraint
for i := range oldParsed.Columns {
oldParsed.Columns[i].Collate = ""
oldParsed.Columns[i].Sort = ""
}
oldParsedStr := oldParsed.Build()
for _, column := range oldParsed.Columns {
@@ -621,19 +627,29 @@ func (cv *collectionValidator) checkIndexes(value any) error {
newParsed := dbutils.ParseIndex(newIndex)
// exclude the non-important identifiers from the check
newParsed.SchemaName = oldParsed.SchemaName
newParsed.IndexName = oldParsed.IndexName
newParsed.TableName = oldParsed.TableName
// exclude partial constraints
newParsed.Where = oldParsed.Where
// reset collate and sort
for i := range newParsed.Columns {
newParsed.Columns[i].Collate = ""
newParsed.Columns[i].Sort = ""
}
if oldParsedStr == newParsed.Build() {
hasMatch = true
continue
break
}
}
if !hasMatch {
return validation.NewError(
"validation_unique_system_field_index_change",
fmt.Sprintf("Unique index definition on system fields (%q) cannot be changed.", f.GetName()),
"validation_invalid_unique_system_field_index",
"Unique index definition on system fields ({{.fieldName}}) is invalid or missing.",
).SetParams(map[string]any{"fieldName": f.GetName()})
}
@@ -653,7 +669,7 @@ func (cv *collectionValidator) checkIndexes(value any) error {
if !dbutils.HasSingleColumnUniqueIndex(name, indexes) {
return validation.NewError(
"validation_missing_required_unique_index",
`Missing required unique index for field "`+name+`".`,
`Missing required unique index for field "{{.fieldName}}".`,
).SetParams(map[string]any{"fieldName": name})
}
}
+88 -4
View File
@@ -550,7 +550,7 @@ func TestCollectionValidate(t *testing.T) {
expectedErrors: []string{"indexes"},
},
{
name: "changing index on system field",
name: "changing partial constraint of existing index on system field",
collection: func(app core.App) (*core.Collection, error) {
demo2, err := app.FindCollectionByNameOrId("demo2")
if err != nil {
@@ -571,7 +571,91 @@ func TestCollectionValidate(t *testing.T) {
// replace the index with a partial one
demo2.RemoveIndex("idx_unique_demo2_title")
demo2.AddIndex("idx_unique_demo2_title", true, "title", "1 = 1")
demo2.AddIndex("idx_new_demo2_title", true, "title", "1 = 1")
return demo2, nil
},
expectedErrors: []string{},
},
{
name: "changing column sort and collate of existing index on system field",
collection: func(app core.App) (*core.Collection, error) {
demo2, err := app.FindCollectionByNameOrId("demo2")
if err != nil {
return nil, err
}
// mark the title field as system
demo2.Fields.GetByName("title").SetSystem(true)
if err = app.Save(demo2); err != nil {
return nil, err
}
// refresh
demo2, err = app.FindCollectionByNameOrId("demo2")
if err != nil {
return nil, err
}
// replace the index with a new one for the same column but with collate and sort
demo2.RemoveIndex("idx_unique_demo2_title")
demo2.AddIndex("idx_new_demo2_title", true, "title COLLATE test ASC", "")
return demo2, nil
},
expectedErrors: []string{},
},
{
name: "adding new column to index on system field",
collection: func(app core.App) (*core.Collection, error) {
demo2, err := app.FindCollectionByNameOrId("demo2")
if err != nil {
return nil, err
}
// mark the title field as system
demo2.Fields.GetByName("title").SetSystem(true)
if err = app.Save(demo2); err != nil {
return nil, err
}
// refresh
demo2, err = app.FindCollectionByNameOrId("demo2")
if err != nil {
return nil, err
}
// replace the index with a non-unique one
demo2.RemoveIndex("idx_unique_demo2_title")
demo2.AddIndex("idx_new_title", false, "title, id", "")
return demo2, nil
},
expectedErrors: []string{"indexes"},
},
{
name: "changing index type on system field",
collection: func(app core.App) (*core.Collection, error) {
demo2, err := app.FindCollectionByNameOrId("demo2")
if err != nil {
return nil, err
}
// mark the title field as system
demo2.Fields.GetByName("title").SetSystem(true)
if err = app.Save(demo2); err != nil {
return nil, err
}
// refresh
demo2, err = app.FindCollectionByNameOrId("demo2")
if err != nil {
return nil, err
}
// replace the index with a non-unique one (partial constraints are ignored)
demo2.RemoveIndex("idx_unique_demo2_title")
demo2.AddIndex("idx_new_title", false, "title", "1=1")
return demo2, nil
},
@@ -620,7 +704,7 @@ func TestCollectionValidate(t *testing.T) {
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("new_auth")
c.Fields = core.NewFieldsList(
&core.TextField{Name: "id", PrimaryKey: true, Required: true},
&core.TextField{Name: "id", PrimaryKey: true, Required: true, Pattern: `\w+`},
)
return c, nil
},
@@ -631,7 +715,7 @@ func TestCollectionValidate(t *testing.T) {
collection: func(app core.App) (*core.Collection, error) {
c := core.NewBaseCollection("new_auth")
c.Fields = core.NewFieldsList(
&core.TextField{Name: "id", PrimaryKey: true, Required: true},
&core.TextField{Name: "id", PrimaryKey: true, Required: true, Pattern: `\w+`},
&core.TextField{Id: "f1", Name: "Test"}, // case-insensitive
&core.BoolField{Id: "f2", Name: "test"},
)
+2 -2
View File
@@ -59,7 +59,7 @@ func GenerateDefaultRandomId() string {
// crc32Checksum generates a stringified crc32 checksum from the provided plain string.
func crc32Checksum(str string) string {
return strconv.Itoa(int(crc32.ChecksumIEEE([]byte(str))))
return strconv.FormatInt(int64(crc32.ChecksumIEEE([]byte(str))), 10)
}
// ModelQuery creates a new preconfigured select app.DB() query with preset
@@ -464,7 +464,7 @@ func validateCollectionId(app App, optTypes ...string) validation.RuleFunc {
return validation.NewError(
"validation_invalid_collection_type",
fmt.Sprintf("Invalid collection type - must be %s.", strings.Join(optTypes, ", ")),
).SetParams(map[string]any{"types": optTypes})
)
}
return nil
+10 -8
View File
@@ -46,14 +46,16 @@ func baseLockRetry(op func(attempt int) error, maxRetries int) error {
Retry:
err := op(attempt)
if err != nil &&
attempt <= maxRetries &&
// we are checking the plain error text to handle both cgo and noncgo errors
strings.Contains(err.Error(), "database is locked") {
// wait and retry
time.Sleep(getDefaultRetryInterval(attempt))
attempt++
goto Retry
if err != nil && attempt <= maxRetries {
errStr := err.Error()
// we are checking the error against the plain error texts since the codes could vary between drivers
if strings.Contains(errStr, "database is locked") ||
strings.Contains(errStr, "table is locked") {
// wait and retry
time.Sleep(getDefaultRetryInterval(attempt))
attempt++
goto Retry
}
}
return err
+1
View File
@@ -33,6 +33,7 @@ func TestBaseLockRetry(t *testing.T) {
{nil, 3, 1},
{errors.New("test"), 3, 1},
{errors.New("database is locked"), 3, 3},
{errors.New("table is locked"), 3, 3},
}
for i, s := range scenarios {
+178
View File
@@ -168,16 +168,28 @@ func TestTransactionHooksCallsOnSuccess(t *testing.T) {
})
app.OnModelAfterCreateSuccess().BindFunc(func(e *core.ModelEvent) error {
if e.App.IsTransactional() {
t.Fatal("Expected e.App to be non-transactional")
}
afterCreateHookCalls++
return e.Next()
})
app.OnModelAfterUpdateSuccess().BindFunc(func(e *core.ModelEvent) error {
if e.App.IsTransactional() {
t.Fatal("Expected e.App to be non-transactional")
}
afterUpdateHookCalls++
return e.Next()
})
app.OnModelAfterDeleteSuccess().BindFunc(func(e *core.ModelEvent) error {
if e.App.IsTransactional() {
t.Fatal("Expected e.App to be non-transactional")
}
afterDeleteHookCalls++
return e.Next()
})
@@ -233,3 +245,169 @@ func TestTransactionHooksCallsOnSuccess(t *testing.T) {
t.Errorf("Expected afterDeleteHookCalls to be called 1 time, got %d", afterDeleteHookCalls)
}
}
func TestTransactionFromInnerCreateHook(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
app.OnRecordCreateExecute("demo2").BindFunc(func(e *core.RecordEvent) error {
originalApp := e.App
return e.App.RunInTransaction(func(txApp core.App) error {
e.App = txApp
defer func() {
e.App = originalApp
}()
nextErr := e.Next()
return nextErr
})
})
app.OnRecordAfterCreateSuccess("demo2").BindFunc(func(e *core.RecordEvent) error {
if e.App.IsTransactional() {
t.Fatal("Expected e.App to be non-transactional")
}
// perform a db query with the app instance to ensure that it is still valid
_, err := e.App.FindFirstRecordByFilter("demo2", "1=1")
if err != nil {
t.Fatalf("Failed to perform a db query after tx success: %v", err)
}
return e.Next()
})
collection, err := app.FindCollectionByNameOrId("demo2")
if err != nil {
t.Fatal(err)
}
record := core.NewRecord(collection)
record.Set("title", "test_inner_tx")
if err = app.Save(record); err != nil {
t.Fatalf("Create failed: %v", err)
}
expectedHookCalls := map[string]int{
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateSuccess": 1,
}
for k, total := range expectedHookCalls {
if found, ok := app.EventCalls[k]; !ok || total != found {
t.Fatalf("Expected %q %d calls, got %d", k, total, found)
}
}
}
func TestTransactionFromInnerUpdateHook(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
app.OnRecordUpdateExecute("demo2").BindFunc(func(e *core.RecordEvent) error {
originalApp := e.App
return e.App.RunInTransaction(func(txApp core.App) error {
e.App = txApp
defer func() {
e.App = originalApp
}()
nextErr := e.Next()
return nextErr
})
})
app.OnRecordAfterUpdateSuccess("demo2").BindFunc(func(e *core.RecordEvent) error {
if e.App.IsTransactional() {
t.Fatal("Expected e.App to be non-transactional")
}
// perform a db query with the app instance to ensure that it is still valid
_, err := e.App.FindFirstRecordByFilter("demo2", "1=1")
if err != nil {
t.Fatalf("Failed to perform a db query after tx success: %v", err)
}
return e.Next()
})
existingModel, err := app.FindFirstRecordByFilter("demo2", "1=1")
if err != nil {
t.Fatal(err)
}
if err = app.Save(existingModel); err != nil {
t.Fatalf("Update failed: %v", err)
}
expectedHookCalls := map[string]int{
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
}
for k, total := range expectedHookCalls {
if found, ok := app.EventCalls[k]; !ok || total != found {
t.Fatalf("Expected %q %d calls, got %d", k, total, found)
}
}
}
func TestTransactionFromInnerDeleteHook(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
app.OnRecordDeleteExecute("demo2").BindFunc(func(e *core.RecordEvent) error {
originalApp := e.App
return e.App.RunInTransaction(func(txApp core.App) error {
e.App = txApp
defer func() {
e.App = originalApp
}()
nextErr := e.Next()
return nextErr
})
})
app.OnRecordAfterDeleteSuccess("demo2").BindFunc(func(e *core.RecordEvent) error {
if e.App.IsTransactional() {
t.Fatal("Expected e.App to be non-transactional")
}
// perform a db query with the app instance to ensure that it is still valid
_, err := e.App.FindFirstRecordByFilter("demo2", "1=1")
if err != nil {
t.Fatalf("Failed to perform a db query after tx success: %v", err)
}
return e.Next()
})
existingModel, err := app.FindFirstRecordByFilter("demo2", "1=1")
if err != nil {
t.Fatal(err)
}
if err = app.Delete(existingModel); err != nil {
t.Fatalf("Delete failed: %v", err)
}
expectedHookCalls := map[string]int{
"OnRecordDeleteExecute": 1,
"OnRecordAfterDeleteSuccess": 1,
}
for k, total := range expectedHookCalls {
if found, ok := app.EventCalls[k]; !ok || total != found {
t.Fatalf("Expected %q %d calls, got %d", k, total, found)
}
}
}
+7 -7
View File
@@ -17,7 +17,7 @@ func TestEventRequestRealIP(t *testing.T) {
"CF-Connecting-IP": {"1.2.3.4", "1.1.1.1"},
"Fly-Client-IP": {"1.2.3.4", "1.1.1.2"},
"X-Real-IP": {"1.2.3.4", "1.1.1.3,1.1.1.4"},
"X-Forward-For": {"1.2.3.4", "invalid,1.1.1.5,1.1.1.6,invalid"},
"X-Forwarded-For": {"1.2.3.4", "invalid,1.1.1.5,1.1.1.6,invalid"},
}
scenarios := []struct {
@@ -44,28 +44,28 @@ func TestEventRequestRealIP(t *testing.T) {
{
"trusted X-Real-IP (rightmost)",
headers,
[]string{"header1", "x-real-ip", "x-forward-for"},
[]string{"header1", "x-real-ip", "x-forwarded-for"},
false,
"1.1.1.4",
},
{
"trusted X-Real-IP (leftmost)",
headers,
[]string{"header1", "x-real-ip", "x-forward-for"},
[]string{"header1", "x-real-ip", "x-forwarded-for"},
true,
"1.1.1.3",
},
{
"trusted X-Forward-For (rightmost)",
"trusted X-Forwarded-For (rightmost)",
headers,
[]string{"header1", "x-forward-for"},
[]string{"header1", "x-forwarded-for"},
false,
"1.1.1.6",
},
{
"trusted X-Forward-For (leftmost)",
"trusted X-Forwarded-For (leftmost)",
headers,
[]string{"header1", "x-forward-for"},
[]string{"header1", "x-forwarded-for"},
true,
"1.1.1.5",
},
+28 -3
View File
@@ -173,8 +173,8 @@ type ModelEvent struct {
}
type ModelErrorEvent struct {
ModelEvent
Error error
ModelEvent
}
// -------------------------------------------------------------------
@@ -196,8 +196,8 @@ type RecordEvent struct {
}
type RecordErrorEvent struct {
RecordEvent
Error error
RecordEvent
}
func syncModelEventWithRecordEvent(me *ModelEvent, re *RecordEvent) {
@@ -216,6 +216,12 @@ func syncModelEventWithRecordEvent(me *ModelEvent, re *RecordEvent) {
// }
}
func syncRecordEventWithModelEvent(re *RecordEvent, me *ModelEvent) {
re.App = me.App
re.Context = me.Context
re.Type = me.Type
}
func newRecordEventFromModelEvent(me *ModelEvent) (*RecordEvent, bool) {
record, ok := me.Model.(*Record)
if !ok {
@@ -253,6 +259,11 @@ func syncModelErrorEventWithRecordErrorEvent(me *ModelErrorEvent, re *RecordErro
me.Error = re.Error
}
func syncRecordErrorEventWithModelErrorEvent(re *RecordErrorEvent, me *ModelErrorEvent) {
syncRecordEventWithModelEvent(&re.RecordEvent, &me.ModelEvent)
me.Error = re.Error
}
// -------------------------------------------------------------------
// Collection events data
// -------------------------------------------------------------------
@@ -272,8 +283,8 @@ type CollectionEvent struct {
}
type CollectionErrorEvent struct {
CollectionEvent
Error error
CollectionEvent
}
func syncModelEventWithCollectionEvent(me *ModelEvent, ce *CollectionEvent) {
@@ -283,6 +294,15 @@ func syncModelEventWithCollectionEvent(me *ModelEvent, ce *CollectionEvent) {
me.Model = ce.Collection
}
func syncCollectionEventWithModelEvent(ce *CollectionEvent, me *ModelEvent) {
ce.App = me.App
ce.Context = me.Context
ce.Type = me.Type
if c, ok := me.Model.(*Collection); ok {
ce.Collection = c
}
}
func newCollectionEventFromModelEvent(me *ModelEvent) (*CollectionEvent, bool) {
record, ok := me.Model.(*Collection)
if !ok {
@@ -316,6 +336,11 @@ func syncModelErrorEventWithCollectionErrorEvent(me *ModelErrorEvent, ce *Collec
me.Error = ce.Error
}
func syncCollectionErrorEventWithModelErrorEvent(ce *CollectionErrorEvent, me *ModelErrorEvent) {
syncCollectionEventWithModelEvent(&ce.CollectionEvent, &me.ModelEvent)
me.Error = ce.Error
}
// -------------------------------------------------------------------
// File API events data
// -------------------------------------------------------------------
+41 -16
View File
@@ -16,6 +16,9 @@ func init() {
const FieldTypeAutodate = "autodate"
// used to keep track of the last set autodate value
const autodateLastKnownPrefix = internalCustomFieldKeyPrefix + "_last_autodate_"
var (
_ Field = (*AutodateField)(nil)
_ SetterFinder = (*AutodateField)(nil)
@@ -167,24 +170,46 @@ func (f *AutodateField) Intercept(
actionFunc func() error,
) error {
switch actionName {
case InterceptorActionCreate:
// ignore for custom date manually set with record.SetRaw()
if f.OnCreate && !f.hasBeenManuallyChanged(record) {
record.SetRaw(f.Name, types.NowDateTime())
case InterceptorActionCreateExecute:
// ignore if a date different from the old one was manually set with SetRaw
if f.OnCreate && record.GetDateTime(f.Name).Equal(f.getLastKnownValue(record)) {
now := types.NowDateTime()
record.SetRaw(f.Name, now)
record.SetRaw(autodateLastKnownPrefix+f.Name, now) // eagerly set so that it can be renewed on resave after failure
}
case InterceptorActionUpdate:
// ignore for custom date manually set with record.SetRaw()
if f.OnUpdate && !f.hasBeenManuallyChanged(record) {
record.SetRaw(f.Name, types.NowDateTime())
if err := actionFunc(); err != nil {
return err
}
record.SetRaw(autodateLastKnownPrefix+f.Name, record.GetRaw(f.Name))
return nil
case InterceptorActionUpdateExecute:
// ignore if a date different from the old one was manually set with SetRaw
if f.OnUpdate && record.GetDateTime(f.Name).Equal(f.getLastKnownValue(record)) {
now := types.NowDateTime()
record.SetRaw(f.Name, now)
record.SetRaw(autodateLastKnownPrefix+f.Name, now) // eagerly set so that it can be renewed on resave after failure
}
if err := actionFunc(); err != nil {
return err
}
record.SetRaw(autodateLastKnownPrefix+f.Name, record.GetRaw(f.Name))
return nil
default:
return actionFunc()
}
}
func (f *AutodateField) getLastKnownValue(record *Record) types.DateTime {
v := record.GetDateTime(autodateLastKnownPrefix + f.Name)
if !v.IsZero() {
return v
}
return actionFunc()
}
func (f *AutodateField) hasBeenManuallyChanged(record *Record) bool {
vNew, _ := record.GetRaw(f.Name).(types.DateTime)
vOld, _ := record.Original().GetRaw(f.Name).(types.DateTime)
return vNew.String() != vOld.String()
return record.Original().GetDateTime(f.Name)
}
+102 -10
View File
@@ -2,6 +2,7 @@ package core_test
import (
"context"
"errors"
"fmt"
"strings"
"testing"
@@ -9,6 +10,7 @@ import (
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tests"
"github.com/pocketbase/pocketbase/tools/hook"
"github.com/pocketbase/pocketbase/tools/types"
)
@@ -226,6 +228,13 @@ func TestAutodateFieldFindSetter(t *testing.T) {
})
}
func cutMilliseconds(datetime string) string {
if len(datetime) > 19 {
return datetime[:19]
}
return datetime
}
func TestAutodateFieldIntercept(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
@@ -255,7 +264,7 @@ func TestAutodateFieldIntercept(t *testing.T) {
},
{
"create with zero value (disabled onCreate)",
core.InterceptorActionCreate,
core.InterceptorActionCreateExecute,
&core.AutodateField{Name: "test", OnCreate: false, OnUpdate: true},
func() *core.Record {
return core.NewRecord(collection)
@@ -264,7 +273,7 @@ func TestAutodateFieldIntercept(t *testing.T) {
},
{
"create with zero value",
core.InterceptorActionCreate,
core.InterceptorActionCreateExecute,
&core.AutodateField{Name: "test", OnCreate: true, OnUpdate: true},
func() *core.Record {
return core.NewRecord(collection)
@@ -273,7 +282,7 @@ func TestAutodateFieldIntercept(t *testing.T) {
},
{
"create with non-zero value",
core.InterceptorActionCreate,
core.InterceptorActionCreateExecute,
&core.AutodateField{Name: "test", OnCreate: true, OnUpdate: true},
func() *core.Record {
record := core.NewRecord(collection)
@@ -284,7 +293,7 @@ func TestAutodateFieldIntercept(t *testing.T) {
},
{
"update with zero value (disabled onUpdate)",
core.InterceptorActionUpdate,
core.InterceptorActionUpdateExecute,
&core.AutodateField{Name: "test", OnCreate: true, OnUpdate: false},
func() *core.Record {
return core.NewRecord(collection)
@@ -293,7 +302,7 @@ func TestAutodateFieldIntercept(t *testing.T) {
},
{
"update with zero value",
core.InterceptorActionUpdate,
core.InterceptorActionUpdateExecute,
&core.AutodateField{Name: "test", OnCreate: true, OnUpdate: true},
func() *core.Record {
return core.NewRecord(collection)
@@ -302,7 +311,7 @@ func TestAutodateFieldIntercept(t *testing.T) {
},
{
"update with non-zero value",
core.InterceptorActionUpdate,
core.InterceptorActionUpdateExecute,
&core.AutodateField{Name: "test", OnCreate: true, OnUpdate: true},
func() *core.Record {
record := core.NewRecord(collection)
@@ -341,9 +350,92 @@ func TestAutodateFieldIntercept(t *testing.T) {
}
}
func cutMilliseconds(datetime string) string {
if len(datetime) > 19 {
return datetime[:19]
func TestAutodateRecordResave(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
collection, err := app.FindCollectionByNameOrId("demo2")
if err != nil {
t.Fatal(err)
}
record, err := app.FindRecordById(collection, "llvuca81nly1qls")
if err != nil {
t.Fatal(err)
}
lastUpdated := record.GetDateTime("updated")
// save with autogenerated date
err = app.Save(record)
if err != nil {
t.Fatal(err)
}
newUpdated := record.GetDateTime("updated")
if newUpdated.Equal(lastUpdated) {
t.Fatalf("[0] Expected updated to change, got %v", newUpdated)
}
lastUpdated = newUpdated
// save with custom date
manualUpdated := lastUpdated.Add(-1 * time.Minute)
record.SetRaw("updated", manualUpdated)
err = app.Save(record)
if err != nil {
t.Fatal(err)
}
newUpdated = record.GetDateTime("updated")
if !newUpdated.Equal(manualUpdated) {
t.Fatalf("[1] Expected updated to be the manual set date %v, got %v", manualUpdated, newUpdated)
}
lastUpdated = newUpdated
// save again with autogenerated date
err = app.Save(record)
if err != nil {
t.Fatal(err)
}
newUpdated = record.GetDateTime("updated")
if newUpdated.Equal(lastUpdated) {
t.Fatalf("[2] Expected updated to change, got %v", newUpdated)
}
lastUpdated = newUpdated
// simulate save failure
app.OnRecordUpdateExecute(collection.Id).Bind(&hook.Handler[*core.RecordEvent]{
Id: "test_failure",
Func: func(*core.RecordEvent) error {
return errors.New("test")
},
Priority: 9999999999, // as latest as possible
})
// save again with autogenerated date (should fail)
err = app.Save(record)
if err == nil {
t.Fatal("Expected save failure")
}
// updated should still be set even after save failure
newUpdated = record.GetDateTime("updated")
if newUpdated.Equal(lastUpdated) {
t.Fatalf("[3] Expected updated to change, got %v", newUpdated)
}
lastUpdated = newUpdated
// cleanup the error and resave again
app.OnRecordUpdateExecute(collection.Id).Unbind("test_failure")
err = app.Save(record)
if err != nil {
t.Fatal(err)
}
newUpdated = record.GetDateTime("updated")
if newUpdated.Equal(lastUpdated) {
t.Fatalf("[4] Expected updated to change, got %v", newUpdated)
}
return datetime
}
+1 -2
View File
@@ -2,7 +2,6 @@ package core
import (
"context"
"fmt"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/pocketbase/core/validators"
@@ -137,7 +136,7 @@ func (f *EditorField) ValidateValue(ctx context.Context, app App, record *Record
if int64(len(val)) > maxSize {
return validation.NewError(
"validation_content_size_limit",
fmt.Sprintf("The maximum allowed content size is %v bytes", maxSize),
"The maximum allowed content size is {{.maxSize}} bytes",
).SetParams(map[string]any{"maxSize": maxSize})
}
+13 -4
View File
@@ -250,13 +250,22 @@ func (f *FileField) ValidateValue(ctx context.Context, app App, record *Record)
addedStrings := f.excludeFiles(existingStrings, oldExistingStrings)
if len(addedStrings) > 0 {
return validation.NewError("validation_invalid_file", "Invalid files:"+strings.Join(cast.ToStringSlice(addedStrings), ", ")).
SetParams(map[string]any{"invalidFiles": addedStrings})
invalidFiles := make([]string, len(addedStrings))
for i, invalid := range addedStrings {
invalidStr := cast.ToString(invalid)
if len(invalidStr) > 250 {
invalidStr = invalidStr[:250]
}
invalidFiles[i] = invalidStr
}
return validation.NewError("validation_invalid_file", "Invalid new files: {{.invalidFiles}}.").
SetParams(map[string]any{"invalidFiles": invalidFiles})
}
maxSelect := f.maxSelect()
if len(files) > maxSelect {
return validation.NewError("validation_too_many_files", fmt.Sprintf("The maximum allowed files is %d", maxSelect)).
return validation.NewError("validation_too_many_files", "The maximum allowed files is {{.maxSelect}}").
SetParams(map[string]any{"maxSelect": maxSelect})
}
@@ -391,7 +400,7 @@ func (f *FileField) Intercept(
}
func (f *FileField) getLatestOldValue(app App, record *Record) any {
if !record.IsNew() {
latestOriginal, err := app.FindRecordById(record.Collection(), record.Id)
latestOriginal, err := app.FindRecordById(record.Collection(), cast.ToString(record.LastSavedPK()))
if err == nil {
return latestOriginal.GetRaw(f.Name)
}
+1 -2
View File
@@ -2,7 +2,6 @@ package core
import (
"context"
"fmt"
"slices"
"strconv"
"strings"
@@ -160,7 +159,7 @@ func (f *JSONField) ValidateValue(ctx context.Context, app App, record *Record)
if int64(len(raw)) > maxSize {
return validation.NewError(
"validation_json_size_limit",
fmt.Sprintf("The maximum allowed JSON size is %v bytes", maxSize),
"The maximum allowed JSON size is {{.maxSize}} bytes",
).SetParams(map[string]any{"maxSize": maxSize})
}
+2 -3
View File
@@ -3,7 +3,6 @@ package core
import (
"context"
"database/sql/driver"
"fmt"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/dbx"
@@ -202,13 +201,13 @@ func (f *RelationField) ValidateValue(ctx context.Context, app App, record *Reco
}
if f.MinSelect > 0 && len(ids) < f.MinSelect {
return validation.NewError("validation_not_enough_values", fmt.Sprintf("Select at least %d", f.MinSelect)).
return validation.NewError("validation_not_enough_values", "Select at least {{.minSelect}}").
SetParams(map[string]any{"minSelect": f.MinSelect})
}
maxSelect := max(f.MaxSelect, 1)
if len(ids) > maxSelect {
return validation.NewError("validation_too_many_values", fmt.Sprintf("Select no more than %d", maxSelect)).
return validation.NewError("validation_too_many_values", "Select no more than {{.maxSelect}}").
SetParams(map[string]any{"maxSelect": maxSelect})
}
+2 -3
View File
@@ -3,7 +3,6 @@ package core
import (
"context"
"database/sql/driver"
"fmt"
"slices"
validation "github.com/go-ozzo/ozzo-validation/v4"
@@ -192,14 +191,14 @@ func (f *SelectField) ValidateValue(ctx context.Context, app App, record *Record
// check max selected items
if len(normalizedVal) > maxSelect {
return validation.NewError("validation_too_many_values", fmt.Sprintf("Select no more than %d", maxSelect)).
return validation.NewError("validation_too_many_values", "Select no more than {{.maxSelect}}").
SetParams(map[string]any{"maxSelect": maxSelect})
}
// check against the allowed values
for _, val := range normalizedVal {
if !slices.Contains(f.Values, val) {
return validation.NewError("validation_invalid_value", "Invalid value "+val).
return validation.NewError("validation_invalid_value", "Invalid value {{.value}}").
SetParams(map[string]any{"value": val})
}
}
+50 -12
View File
@@ -2,10 +2,14 @@ package core
import (
"context"
"database/sql"
"errors"
"fmt"
"regexp"
"strings"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core/validators"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/spf13/cast"
@@ -151,6 +155,8 @@ func (f *TextField) PrepareValue(record *Record, raw any) (any, error) {
return cast.ToString(raw), nil
}
var forbiddenPKChars = []string{"/", "\\"}
// ValidateValue implements [Field.ValidateValue] interface method.
func (f *TextField) ValidateValue(ctx context.Context, app App, record *Record) error {
newVal, ok := record.GetRaw(f.Name).(string)
@@ -158,14 +164,46 @@ func (f *TextField) ValidateValue(ctx context.Context, app App, record *Record)
return validators.ErrUnsupportedValueType
}
// disallow PK change
if f.PrimaryKey && !record.IsNew() {
oldVal := record.LastSavedPK()
if oldVal != newVal {
return validation.NewError("validation_pk_change", "The record primary key cannot be changed.")
}
if oldVal != "" {
return nil // no need to further validate since the id can't be updated anyway
if f.PrimaryKey {
// disallow PK change
if !record.IsNew() {
oldVal := record.LastSavedPK()
if oldVal != newVal {
return validation.NewError("validation_pk_change", "The record primary key cannot be changed.")
}
if oldVal != "" {
// no need to further validate because the id can't be updated
// and because the id could have been inserted manually by migration from another system
// that may not comply with the user defined PocketBase validations
return nil
}
} else {
// disallow PK special characters no matter of the Pattern validator to minimize
// side-effects when the primary key is used for example in a directory path
for _, c := range forbiddenPKChars {
if strings.Contains(newVal, c) {
return validation.NewError("validation_pk_forbidden", "The record primary key contains forbidden characters.").
SetParams(map[string]any{"forbidden": c})
}
}
// this technically shouldn't be necessarily but again to
// minimize misuse of the Pattern validator that could cause
// side-effects on some platforms check for duplicates in a case-insensitive manner
//
// (@todo eventually may get replaced in the future with a system unique constraint to avoid races or wrapping the request in a transaction)
if f.Pattern != defaultLowercaseRecordIdPattern {
var exists bool
err := app.DB().
Select("(1)").
From(record.TableName()).
Where(dbx.NewExp("id = {:id} COLLATE NOCASE", dbx.Params{"id": strings.ToLower(newVal)})).
Limit(1).
Row(&exists)
if exists || (err != nil && !errors.Is(err, sql.ErrNoRows)) {
return validation.NewError("validation_pk_invalid", "The record primary key is invalid or already exists.")
}
}
}
}
@@ -188,7 +226,7 @@ func (f *TextField) ValidatePlainValue(value string) error {
length := len([]rune(value))
if f.Min > 0 && length < f.Min {
return validation.NewError("validation_min_text_constraint", fmt.Sprintf("Must be at least %d character(s)", f.Min)).
return validation.NewError("validation_min_text_constraint", "Must be at least {{.min}} character(s)").
SetParams(map[string]any{"min": f.Min})
}
@@ -198,8 +236,8 @@ func (f *TextField) ValidatePlainValue(value string) error {
}
if max > 0 && length > max {
return validation.NewError("validation_max_text_constraint", fmt.Sprintf("Must be less than %d character(s)", max)).
SetParams(map[string]any{"max": f.Max})
return validation.NewError("validation_max_text_constraint", "Must be no more than {{.max}} character(s)").
SetParams(map[string]any{"max": max})
}
if f.Pattern != "" {
@@ -223,7 +261,7 @@ func (f *TextField) ValidateSettings(ctx context.Context, app App, collection *C
validation.Field(&f.PrimaryKey, validation.By(f.checkOtherFieldsForPK(collection))),
validation.Field(&f.Min, validation.Min(0)),
validation.Field(&f.Max, validation.Min(f.Min)),
validation.Field(&f.Pattern, validation.By(validators.IsRegex)),
validation.Field(&f.Pattern, validation.When(f.PrimaryKey, validation.Required), validation.By(validators.IsRegex)),
validation.Field(&f.Hidden, validation.When(f.PrimaryKey, validation.Empty)),
validation.Field(&f.Required, validation.When(f.PrimaryKey, validation.Required)),
validation.Field(&f.AutogeneratePattern, validation.By(validators.IsRegex), validation.By(f.checkAutogeneratePattern)),
+76 -2
View File
@@ -68,7 +68,15 @@ func TestTextFieldValidateValue(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
collection := core.NewBaseCollection("test_collection")
collection, err := app.FindCollectionByNameOrId("demo1")
if err != nil {
t.Fatal(err)
}
existingRecord, err := app.FindFirstRecordByFilter(collection, "id != ''")
if err != nil {
t.Fatal(err)
}
scenarios := []struct {
name string
@@ -116,6 +124,46 @@ func TestTextFieldValidateValue(t *testing.T) {
},
false,
},
{
"special forbidden character / (non-primaryKey)",
&core.TextField{Name: "test", PrimaryKey: false},
func() *core.Record {
record := core.NewRecord(collection)
record.SetRaw("test", "/")
return record
},
false,
},
{
"special forbidden character \\ (non-primaryKey)",
&core.TextField{Name: "test", PrimaryKey: false},
func() *core.Record {
record := core.NewRecord(collection)
record.SetRaw("test", "\\")
return record
},
false,
},
{
"special forbidden character / (primaryKey)",
&core.TextField{Name: "test", PrimaryKey: true},
func() *core.Record {
record := core.NewRecord(collection)
record.SetRaw("test", "/")
return record
},
true,
},
{
"special forbidden character \\ (primaryKey)",
&core.TextField{Name: "test", PrimaryKey: true},
func() *core.Record {
record := core.NewRecord(collection)
record.SetRaw("test", "\\")
return record
},
true,
},
{
"zero field value (primaryKey)",
&core.TextField{Name: "test", PrimaryKey: true},
@@ -131,11 +179,21 @@ func TestTextFieldValidateValue(t *testing.T) {
&core.TextField{Name: "test", PrimaryKey: true},
func() *core.Record {
record := core.NewRecord(collection)
record.SetRaw("test", "abc")
record.SetRaw("test", "abcd")
return record
},
false,
},
{
"case-insensitive duplicated primary key check",
&core.TextField{Name: "test", PrimaryKey: true},
func() *core.Record {
record := core.NewRecord(collection)
record.SetRaw("test", strings.ToUpper(existingRecord.Id))
return record
},
true,
},
{
"< min",
&core.TextField{Name: "test", Min: 4},
@@ -259,10 +317,23 @@ func TestTextFieldValidateSettings(t *testing.T) {
Id: "test",
Name: "id",
PrimaryKey: true,
Pattern: `\d+`,
}
},
[]string{"required"},
},
{
"primaryKey without pattern",
func() *core.TextField {
return &core.TextField{
Id: "test",
Name: "id",
PrimaryKey: true,
Required: true,
}
},
[]string{"pattern"},
},
{
"primaryKey with hidden",
func() *core.TextField {
@@ -272,6 +343,7 @@ func TestTextFieldValidateSettings(t *testing.T) {
Required: true,
PrimaryKey: true,
Hidden: true,
Pattern: `\d+`,
}
},
[]string{"hidden"},
@@ -284,6 +356,7 @@ func TestTextFieldValidateSettings(t *testing.T) {
Name: "test",
PrimaryKey: true,
Required: true,
Pattern: `\d+`,
}
},
[]string{"name"},
@@ -295,6 +368,7 @@ func TestTextFieldValidateSettings(t *testing.T) {
Id: "test2",
Name: "id",
PrimaryKey: true,
Pattern: `\d+`,
Required: true,
}
},
+92 -19
View File
@@ -4,6 +4,7 @@ import (
"database/sql/driver"
"encoding/json"
"fmt"
"slices"
"strconv"
)
@@ -12,7 +13,7 @@ func NewFieldsList(fields ...Field) FieldsList {
l := make(FieldsList, 0, len(fields))
for _, f := range fields {
l.Add(f)
l.add(-1, f)
}
return l
@@ -116,7 +117,26 @@ func (l *FieldsList) RemoveByName(fieldName string) {
// (the id value doesn't really matter and it is mostly used as a stable identifier in case of a field rename).
func (l *FieldsList) Add(fields ...Field) {
for _, f := range fields {
l.add(f)
l.add(-1, f)
}
}
// AddAt is the same as Add but insert/move the fields at the specific position.
//
// If pos < 0, then this method acts the same as calling Add.
//
// If pos > FieldsList total items, then the specified fields are inserted/moved at the end of the list.
func (l *FieldsList) AddAt(pos int, fields ...Field) {
total := len(*l)
for i, f := range fields {
if pos < 0 {
l.add(-1, f)
} else if pos > total {
l.add(total+i, f)
} else {
l.add(pos+i, f)
}
}
}
@@ -132,13 +152,42 @@ func (l *FieldsList) Add(fields ...Field) {
// l.AddMarshaledJSON([]byte{`{"type":"text", name: "test"}`})
// l.AddMarshaledJSON([]byte{`[{"type":"text", name: "test1"}, {"type":"text", name: "test2"}]`})
func (l *FieldsList) AddMarshaledJSON(rawJSON []byte) error {
extractedFields, err := marshaledJSONtoFieldsList(rawJSON)
if err != nil {
return err
}
l.Add(extractedFields...)
return nil
}
// AddMarshaledJSONAt is the same as AddMarshaledJSON but insert/move the fields at the specific position.
//
// If pos < 0, then this method acts the same as calling AddMarshaledJSON.
//
// If pos > FieldsList total items, then the specified fields are inserted/moved at the end of the list.
func (l *FieldsList) AddMarshaledJSONAt(pos int, rawJSON []byte) error {
extractedFields, err := marshaledJSONtoFieldsList(rawJSON)
if err != nil {
return err
}
l.AddAt(pos, extractedFields...)
return nil
}
func marshaledJSONtoFieldsList(rawJSON []byte) (FieldsList, error) {
extractedFields := FieldsList{}
// nothing to add
if len(rawJSON) == 0 {
return nil // nothing to add
return extractedFields, nil
}
// try to unmarshal first into a new fieds list
// (assuming that rawJSON is array of objects)
extractedFields := FieldsList{}
err := json.Unmarshal(rawJSON, &extractedFields)
if err != nil {
// try again but wrap the rawJSON in []
@@ -149,21 +198,25 @@ func (l *FieldsList) AddMarshaledJSON(rawJSON []byte) error {
wrapped = append(wrapped, ']')
err = json.Unmarshal(wrapped, &extractedFields)
if err != nil {
return fmt.Errorf("failed to unmarshal the provided JSON - expects array of objects or just single object: %w", err)
return nil, fmt.Errorf("failed to unmarshal the provided JSON - expects array of objects or just single object: %w", err)
}
}
for _, f := range extractedFields {
l.add(f)
}
return nil
return extractedFields, nil
}
func (l *FieldsList) add(newField Field) {
func (l *FieldsList) add(pos int, newField Field) {
fields := *l
var replaceByName bool
var replaceInPlace bool
if pos < 0 {
replaceInPlace = true
pos = len(fields)
} else if pos > len(fields) {
pos = len(fields)
}
newFieldId := newField.GetId()
@@ -182,24 +235,44 @@ func (l *FieldsList) add(newField Field) {
newField.SetId(newFieldId)
}
// replace existing
// try to replace existing
for i, field := range fields {
if replaceByName {
if name := newField.GetName(); name != "" && field.GetName() == name {
// reuse the original id
newField.SetId(field.GetId())
(*l)[i] = newField
return
if replaceInPlace {
(*l)[i] = newField
return
} else {
// remove the current field and insert it later at the specific position
*l = slices.Delete(*l, i, i+1)
if total := len(*l); pos > total {
pos = total
}
break
}
}
} else {
if field.GetId() == newFieldId {
(*l)[i] = newField
return
if replaceInPlace {
(*l)[i] = newField
return
} else {
// remove the current field and insert it later at the specific position
*l = slices.Delete(*l, i, i+1)
if total := len(*l); pos > total {
pos = total
}
break
}
}
}
}
// add new field
*l = append(fields, newField)
// insert the new field
*l = slices.Insert(*l, pos, newField)
}
// String returns the string representation of the current list.
@@ -252,7 +325,7 @@ func (l *FieldsList) UnmarshalJSON(data []byte) error {
*l = []Field{} // reset
for _, fwt := range fwts {
l.Add(fwt.Field)
l.add(-1, fwt.Field)
}
return nil
+93
View File
@@ -1,7 +1,10 @@
package core_test
import (
"bytes"
"encoding/json"
"slices"
"strconv"
"strings"
"testing"
@@ -301,6 +304,96 @@ func TestFieldsListAddMarshaledJSON(t *testing.T) {
}
}
func TestFieldsListAddAt(t *testing.T) {
scenarios := []struct {
position int
expected []string
}{
{-2, []string{"test1", "test2_new", "test3", "test4"}},
{-1, []string{"test1", "test2_new", "test3", "test4"}},
{0, []string{"test2_new", "test4", "test1", "test3"}},
{1, []string{"test1", "test2_new", "test4", "test3"}},
{2, []string{"test1", "test3", "test2_new", "test4"}},
{3, []string{"test1", "test3", "test2_new", "test4"}},
{4, []string{"test1", "test3", "test2_new", "test4"}},
{5, []string{"test1", "test3", "test2_new", "test4"}},
}
for _, s := range scenarios {
t.Run(strconv.Itoa(s.position), func(t *testing.T) {
f1 := &core.TextField{Id: "f1Id", Name: "test1"}
f2 := &core.TextField{Id: "f2Id", Name: "test2"}
f3 := &core.TextField{Id: "f3Id", Name: "test3"}
testFieldsList := core.NewFieldsList(f1, f2, f3)
f2New := &core.EmailField{Id: "f2Id", Name: "test2_new"}
f4 := &core.URLField{Name: "test4"}
testFieldsList.AddAt(s.position, f2New, f4)
rawNames, err := json.Marshal(testFieldsList.FieldNames())
if err != nil {
t.Fatal(err)
}
rawExpected, err := json.Marshal(s.expected)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(rawNames, rawExpected) {
t.Fatalf("Expected fields\n%s\ngot\n%s", rawExpected, rawNames)
}
})
}
}
func TestFieldsListAddMarshaledJSONAt(t *testing.T) {
scenarios := []struct {
position int
expected []string
}{
{-2, []string{"test1", "test2_new", "test3", "test4"}},
{-1, []string{"test1", "test2_new", "test3", "test4"}},
{0, []string{"test2_new", "test4", "test1", "test3"}},
{1, []string{"test1", "test2_new", "test4", "test3"}},
{2, []string{"test1", "test3", "test2_new", "test4"}},
{3, []string{"test1", "test3", "test2_new", "test4"}},
{4, []string{"test1", "test3", "test2_new", "test4"}},
{5, []string{"test1", "test3", "test2_new", "test4"}},
}
for _, s := range scenarios {
t.Run(strconv.Itoa(s.position), func(t *testing.T) {
f1 := &core.TextField{Id: "f1Id", Name: "test1"}
f2 := &core.TextField{Id: "f2Id", Name: "test2"}
f3 := &core.TextField{Id: "f3Id", Name: "test3"}
testFieldsList := core.NewFieldsList(f1, f2, f3)
err := testFieldsList.AddMarshaledJSONAt(s.position, []byte(`[
{"id":"f2Id", "name":"test2_new", "type": "text"},
{"name": "test4", "type": "text"}
]`))
if err != nil {
t.Fatal(err)
}
rawNames, err := json.Marshal(testFieldsList.FieldNames())
if err != nil {
t.Fatal(err)
}
rawExpected, err := json.Marshal(s.expected)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(rawNames, rawExpected) {
t.Fatalf("Expected fields\n%s\ngot\n%s", rawExpected, rawNames)
}
})
}
}
func TestFieldsListStringAndValue(t *testing.T) {
t.Run("empty list", func(t *testing.T) {
testFieldsList := core.NewFieldsList()
+81 -23
View File
@@ -55,10 +55,13 @@ func (app *BaseApp) registerRecordHooks() {
Id: systemHookIdRecord,
Func: func(me *ModelEvent) error {
if re, ok := newRecordEventFromModelEvent(me); ok {
return me.App.OnRecordValidate().Trigger(re, func(re *RecordEvent) error {
err := me.App.OnRecordValidate().Trigger(re, func(re *RecordEvent) error {
syncModelEventWithRecordEvent(me, re)
defer syncRecordEventWithModelEvent(re, me)
return me.Next()
})
syncModelEventWithRecordEvent(me, re)
return err
}
return me.Next()
@@ -70,10 +73,13 @@ func (app *BaseApp) registerRecordHooks() {
Id: systemHookIdRecord,
Func: func(me *ModelEvent) error {
if re, ok := newRecordEventFromModelEvent(me); ok {
return me.App.OnRecordCreate().Trigger(re, func(re *RecordEvent) error {
err := me.App.OnRecordCreate().Trigger(re, func(re *RecordEvent) error {
syncModelEventWithRecordEvent(me, re)
defer syncRecordEventWithModelEvent(re, me)
return me.Next()
})
syncModelEventWithRecordEvent(me, re)
return err
}
return me.Next()
@@ -85,10 +91,13 @@ func (app *BaseApp) registerRecordHooks() {
Id: systemHookIdRecord,
Func: func(me *ModelEvent) error {
if re, ok := newRecordEventFromModelEvent(me); ok {
return me.App.OnRecordCreateExecute().Trigger(re, func(re *RecordEvent) error {
err := me.App.OnRecordCreateExecute().Trigger(re, func(re *RecordEvent) error {
syncModelEventWithRecordEvent(me, re)
defer syncRecordEventWithModelEvent(re, me)
return me.Next()
})
syncModelEventWithRecordEvent(me, re)
return err
}
return me.Next()
@@ -100,10 +109,13 @@ func (app *BaseApp) registerRecordHooks() {
Id: systemHookIdRecord,
Func: func(me *ModelEvent) error {
if re, ok := newRecordEventFromModelEvent(me); ok {
return me.App.OnRecordAfterCreateSuccess().Trigger(re, func(re *RecordEvent) error {
err := me.App.OnRecordAfterCreateSuccess().Trigger(re, func(re *RecordEvent) error {
syncModelEventWithRecordEvent(me, re)
defer syncRecordEventWithModelEvent(re, me)
return me.Next()
})
syncModelEventWithRecordEvent(me, re)
return err
}
return me.Next()
@@ -115,10 +127,13 @@ func (app *BaseApp) registerRecordHooks() {
Id: systemHookIdRecord,
Func: func(me *ModelErrorEvent) error {
if re, ok := newRecordErrorEventFromModelErrorEvent(me); ok {
return me.App.OnRecordAfterCreateError().Trigger(re, func(re *RecordErrorEvent) error {
err := me.App.OnRecordAfterCreateError().Trigger(re, func(re *RecordErrorEvent) error {
syncModelErrorEventWithRecordErrorEvent(me, re)
defer syncRecordErrorEventWithModelErrorEvent(re, me)
return me.Next()
})
syncModelErrorEventWithRecordErrorEvent(me, re)
return err
}
return me.Next()
@@ -130,10 +145,13 @@ func (app *BaseApp) registerRecordHooks() {
Id: systemHookIdRecord,
Func: func(me *ModelEvent) error {
if re, ok := newRecordEventFromModelEvent(me); ok {
return me.App.OnRecordUpdate().Trigger(re, func(re *RecordEvent) error {
err := me.App.OnRecordUpdate().Trigger(re, func(re *RecordEvent) error {
syncModelEventWithRecordEvent(me, re)
defer syncRecordEventWithModelEvent(re, me)
return me.Next()
})
syncModelEventWithRecordEvent(me, re)
return err
}
return me.Next()
@@ -145,10 +163,13 @@ func (app *BaseApp) registerRecordHooks() {
Id: systemHookIdRecord,
Func: func(me *ModelEvent) error {
if re, ok := newRecordEventFromModelEvent(me); ok {
return me.App.OnRecordUpdateExecute().Trigger(re, func(re *RecordEvent) error {
err := me.App.OnRecordUpdateExecute().Trigger(re, func(re *RecordEvent) error {
syncModelEventWithRecordEvent(me, re)
defer syncRecordEventWithModelEvent(re, me)
return me.Next()
})
syncModelEventWithRecordEvent(me, re)
return err
}
return me.Next()
@@ -160,10 +181,13 @@ func (app *BaseApp) registerRecordHooks() {
Id: systemHookIdRecord,
Func: func(me *ModelEvent) error {
if re, ok := newRecordEventFromModelEvent(me); ok {
return me.App.OnRecordAfterUpdateSuccess().Trigger(re, func(re *RecordEvent) error {
err := me.App.OnRecordAfterUpdateSuccess().Trigger(re, func(re *RecordEvent) error {
syncModelEventWithRecordEvent(me, re)
defer syncRecordEventWithModelEvent(re, me)
return me.Next()
})
syncModelEventWithRecordEvent(me, re)
return err
}
return me.Next()
@@ -175,10 +199,13 @@ func (app *BaseApp) registerRecordHooks() {
Id: systemHookIdRecord,
Func: func(me *ModelErrorEvent) error {
if re, ok := newRecordErrorEventFromModelErrorEvent(me); ok {
return me.App.OnRecordAfterUpdateError().Trigger(re, func(re *RecordErrorEvent) error {
err := me.App.OnRecordAfterUpdateError().Trigger(re, func(re *RecordErrorEvent) error {
syncModelErrorEventWithRecordErrorEvent(me, re)
defer syncRecordErrorEventWithModelErrorEvent(re, me)
return me.Next()
})
syncModelErrorEventWithRecordErrorEvent(me, re)
return err
}
return me.Next()
@@ -190,10 +217,13 @@ func (app *BaseApp) registerRecordHooks() {
Id: systemHookIdRecord,
Func: func(me *ModelEvent) error {
if re, ok := newRecordEventFromModelEvent(me); ok {
return me.App.OnRecordDelete().Trigger(re, func(re *RecordEvent) error {
err := me.App.OnRecordDelete().Trigger(re, func(re *RecordEvent) error {
syncModelEventWithRecordEvent(me, re)
defer syncRecordEventWithModelEvent(re, me)
return me.Next()
})
syncModelEventWithRecordEvent(me, re)
return err
}
return me.Next()
@@ -205,10 +235,13 @@ func (app *BaseApp) registerRecordHooks() {
Id: systemHookIdRecord,
Func: func(me *ModelEvent) error {
if re, ok := newRecordEventFromModelEvent(me); ok {
return me.App.OnRecordDeleteExecute().Trigger(re, func(re *RecordEvent) error {
err := me.App.OnRecordDeleteExecute().Trigger(re, func(re *RecordEvent) error {
syncModelEventWithRecordEvent(me, re)
defer syncRecordEventWithModelEvent(re, me)
return me.Next()
})
syncModelEventWithRecordEvent(me, re)
return err
}
return me.Next()
@@ -220,10 +253,13 @@ func (app *BaseApp) registerRecordHooks() {
Id: systemHookIdRecord,
Func: func(me *ModelEvent) error {
if re, ok := newRecordEventFromModelEvent(me); ok {
return me.App.OnRecordAfterDeleteSuccess().Trigger(re, func(re *RecordEvent) error {
err := me.App.OnRecordAfterDeleteSuccess().Trigger(re, func(re *RecordEvent) error {
syncModelEventWithRecordEvent(me, re)
defer syncRecordEventWithModelEvent(re, me)
return me.Next()
})
syncModelEventWithRecordEvent(me, re)
return err
}
return me.Next()
@@ -235,10 +271,13 @@ func (app *BaseApp) registerRecordHooks() {
Id: systemHookIdRecord,
Func: func(me *ModelErrorEvent) error {
if re, ok := newRecordErrorEventFromModelErrorEvent(me); ok {
return me.App.OnRecordAfterDeleteError().Trigger(re, func(re *RecordErrorEvent) error {
err := me.App.OnRecordAfterDeleteError().Trigger(re, func(re *RecordErrorEvent) error {
syncModelErrorEventWithRecordErrorEvent(me, re)
defer syncRecordErrorEventWithModelErrorEvent(re, me)
return me.Next()
})
syncModelErrorEventWithRecordErrorEvent(me, re)
return err
}
return me.Next()
@@ -504,12 +543,16 @@ func NewRecord(collection *Collection) *Record {
}
// initialize default field values
var fieldName string
for _, field := range collection.Fields {
if field.GetName() == FieldNameId {
fieldName = field.GetName()
if fieldName == FieldNameId {
continue
}
value, _ := field.PrepareValue(record, nil)
record.originalData[field.GetName()] = value
record.originalData[fieldName] = value
}
return record
@@ -589,8 +632,12 @@ func (m *Record) Original() *Record {
func (m *Record) Fresh() *Record {
newRecord := m.Original()
// note: this will also load the Id field though m.Get
newRecord.Load(m.FieldsData())
// note: this will also load the Id field through m.GetRaw
var fieldName string
for _, field := range m.collection.Fields {
fieldName = field.GetName()
newRecord.SetRaw(fieldName, m.GetRaw(fieldName))
}
return newRecord
}
@@ -609,7 +656,10 @@ func (m *Record) Clone() *Record {
newRecord.ignoreUnchangedFields = m.ignoreUnchangedFields
newRecord.customVisibility.Reset(m.customVisibility.GetAll())
newRecord.Load(m.data.GetAll())
data := m.data.GetAll()
for k, v := range data {
newRecord.SetRaw(k, v)
}
if m.expand != nil {
newRecord.SetExpand(m.expand.GetAll())
@@ -722,8 +772,10 @@ func (m *Record) MergeExpand(expand map[string]any) {
func (m *Record) FieldsData() map[string]any {
result := make(map[string]any, len(m.collection.Fields))
var fieldName string
for _, field := range m.collection.Fields {
result[field.GetName()] = m.Get(field.GetName())
fieldName = field.GetName()
result[fieldName] = m.Get(fieldName)
}
return result
@@ -1057,15 +1109,18 @@ func (m *Record) dbExport() (map[string]any, error) {
result := make(map[string]any, len(fields))
var fieldName string
for _, field := range fields {
fieldName = field.GetName()
if f, ok := field.(DriverValuer); ok {
v, err := f.DriverValue(m)
if err != nil {
return nil, err
}
result[field.GetName()] = v
result[fieldName] = v
} else {
result[field.GetName()] = m.GetRaw(field.GetName())
result[fieldName] = m.GetRaw(fieldName)
}
}
@@ -1191,8 +1246,11 @@ func (record *Record) PublicExport() map[string]any {
customVisibility := record.customVisibility.GetAll()
// export schema fields
var fieldName string
for _, f := range record.collection.Fields {
isVisible, hasCustomVisibility = customVisibility[f.GetName()]
fieldName = f.GetName()
isVisible, hasCustomVisibility = customVisibility[fieldName]
if !hasCustomVisibility {
isVisible = !f.GetHidden()
}
@@ -1201,7 +1259,7 @@ func (record *Record) PublicExport() map[string]any {
continue
}
export[f.GetName()] = record.Get(f.GetName())
export[fieldName] = record.Get(fieldName)
}
// export custom fields
+30 -4
View File
@@ -244,10 +244,13 @@ func TestRecordFresh(t *testing.T) {
extraFieldsCheck := []string{`"email":`, `"custom":`}
autodateTest := types.NowDateTime()
// change the fields
record.Id = "changed"
record.Set("name", "name_new")
record.Set("custom", "test_custom")
record.SetRaw("created", autodateTest)
record.SetExpand(map[string]any{"test": 123})
record.IgnoreEmailVisibility(true)
record.IgnoreUnchangedFields(true)
@@ -270,6 +273,9 @@ func TestRecordFresh(t *testing.T) {
if v := record.GetString("name"); v != "name_new" {
t.Fatalf("Expected name to be %q, got %q", "name_new", v)
}
if v := record.GetDateTime("created").String(); v != autodateTest.String() {
t.Fatalf("Expected created to be %q, got %q", autodateTest.String(), v)
}
if v := record.GetString("custom"); v != "test_custom" {
t.Fatalf("Expected custom to be %q, got %q", "test_custom", v)
}
@@ -287,6 +293,12 @@ func TestRecordFresh(t *testing.T) {
if v := record.Fresh().GetString("name"); v != record.GetString("name") {
t.Fatalf("Expected the fresh name to be %q, got %q", record.GetString("name"), v)
}
if v := record.Fresh().GetDateTime("created").String(); v != autodateTest.String() {
t.Fatalf("Expected the fresh created to be %q, got %q", autodateTest.String(), v)
}
if v := record.Fresh().GetDateTime("updated").String(); v != record.GetDateTime("updated").String() {
t.Fatalf("Expected the fresh updated to be %q, got %q", record.GetDateTime("updated").String(), v)
}
if v := record.Fresh().GetString("custom"); v != "" {
t.Fatalf("Expected the fresh custom to be %q, got %q", "", v)
}
@@ -321,10 +333,13 @@ func TestRecordClone(t *testing.T) {
extraFieldsCheck := []string{`"email":`, `"custom":`}
autodateTest := types.NowDateTime()
// change the fields
record.Id = "changed"
record.Set("name", "name_new")
record.Set("custom", "test_custom")
record.SetRaw("created", autodateTest)
record.SetExpand(map[string]any{"test": 123})
record.IgnoreEmailVisibility(true)
record.WithCustomData(true)
@@ -346,6 +361,9 @@ func TestRecordClone(t *testing.T) {
if v := record.GetString("name"); v != "name_new" {
t.Fatalf("Expected name to be %q, got %q", "name_new", v)
}
if v := record.GetDateTime("created").String(); v != autodateTest.String() {
t.Fatalf("Expected created to be %q, got %q", autodateTest.String(), v)
}
if v := record.GetString("custom"); v != "test_custom" {
t.Fatalf("Expected custom to be %q, got %q", "test_custom", v)
}
@@ -363,6 +381,12 @@ func TestRecordClone(t *testing.T) {
if v := record.Clone().GetString("name"); v != record.GetString("name") {
t.Fatalf("Expected the clone name to be %q, got %q", record.GetString("name"), v)
}
if v := record.Clone().GetDateTime("created").String(); v != autodateTest.String() {
t.Fatalf("Expected the clone created to be %q, got %q", autodateTest.String(), v)
}
if v := record.Clone().GetDateTime("updated").String(); v != record.GetDateTime("updated").String() {
t.Fatalf("Expected the clone updated to be %q, got %q", record.GetDateTime("updated").String(), v)
}
if v := record.Clone().GetString("custom"); v != "test_custom" {
t.Fatalf("Expected the clone custom to be %q, got %q", "test_custom", v)
}
@@ -1564,13 +1588,15 @@ func TestRecordValidate(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
collection := core.NewBaseCollection("test")
// dummy collection to ensure that the specified field validators are triggered
collection := core.NewBaseCollection("validate_test")
collection.Fields.Add(
// dummy fields to ensure that its validators are triggered
&core.TextField{Name: "f1", Min: 3},
&core.NumberField{Name: "f2", Required: true},
)
if err := app.Save(collection); err != nil {
t.Fatal(err)
}
record := core.NewRecord(collection)
record.Id = "!invalid"
@@ -1585,7 +1611,7 @@ func TestRecordValidate(t *testing.T) {
})
t.Run("satisfying the fields validations", func(t *testing.T) {
record.Id = strings.Repeat("a", 15)
record.Id = strings.Repeat("b", 15)
record.Set("f1", "abc")
record.Set("f2", 1)
tests.TestValidationErrors(t, app.Validate(record), nil)
+10 -10
View File
@@ -102,11 +102,6 @@ func (app *BaseApp) RecordQuery(collectionModelOrIdentifier any) *dbx.SelectQuer
return nil
default: // expects []RecordProxy slice
records, err := resolveRecordAllHook(collection, op)
if err != nil {
return err
}
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
return errors.New("must be a pointer")
@@ -118,11 +113,6 @@ func (app *BaseApp) RecordQuery(collectionModelOrIdentifier any) *dbx.SelectQuer
return errors.New("must be a slice of RecordSetters")
}
// create an empty slice
if rv.IsNil() {
rv.Set(reflect.MakeSlice(rv.Type(), 0, len(records)))
}
et := rv.Type().Elem()
var isSliceOfPointers bool
@@ -135,6 +125,16 @@ func (app *BaseApp) RecordQuery(collectionModelOrIdentifier any) *dbx.SelectQuer
return op(sliceA)
}
records, err := resolveRecordAllHook(collection, op)
if err != nil {
return err
}
// create an empty slice
if rv.IsNil() {
rv.Set(reflect.MakeSlice(rv.Type(), 0, len(records)))
}
for _, record := range records {
ev := reflect.New(et)
+33 -3
View File
@@ -66,7 +66,7 @@ func TestRecordQueryOne(t *testing.T) {
name string
collection string
recordId string
model core.Model
model any
}{
{
"record model",
@@ -82,6 +82,14 @@ func TestRecordQueryOne(t *testing.T) {
core.BaseRecordProxy
}{},
},
{
"custom struct",
"demo1",
"84nmscqy84lsi1t",
&struct {
Id string `db:"id" json:"id"`
}{},
},
}
for _, s := range scenarios {
@@ -98,8 +106,14 @@ func TestRecordQueryOne(t *testing.T) {
t.Fatal(err)
}
if s.model.PK() != s.recordId {
t.Fatalf("Expected record with id %q, got %q", s.recordId, s.model.PK())
raw, err := json.Marshal(s.model)
if err != nil {
t.Fatal(err)
}
rawStr := string(raw)
if !strings.Contains(rawStr, fmt.Sprintf(`"id":%q`, s.recordId)) {
t.Fatalf("Missing id %q in\n%s", s.recordId, rawStr)
}
})
}
@@ -111,6 +125,10 @@ func TestRecordQueryAll(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
type customStructs struct {
Id string `db:"id" json:"id"`
}
type mockRecordProxy struct {
core.BaseRecordProxy
}
@@ -145,6 +163,18 @@ func TestRecordQueryAll(t *testing.T) {
[]any{"84nmscqy84lsi1t", "al1h9ijdeojtsjy"},
&[]mockRecordProxy{},
},
{
"slice of custom structs",
"demo1",
[]any{"84nmscqy84lsi1t", "al1h9ijdeojtsjy"},
&[]customStructs{},
},
{
"slice of pointer custom structs",
"demo1",
[]any{"84nmscqy84lsi1t", "al1h9ijdeojtsjy"},
&[]customStructs{},
},
}
for _, s := range scenarios {
+11 -5
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"os"
"regexp"
"slices"
"strconv"
"strings"
"sync"
@@ -529,7 +530,7 @@ type TrustedProxyConfig struct {
// UseLeftmostIP specifies to use the left-mostish IP from the trusted headers.
//
// Note that this could be insecure when used with X-Forward-For header
// Note that this could be insecure when used with X-Forwarded-For header
// because some proxies like AWS ELB allow users to prepend their own header value
// before appending the trusted ones.
UseLeftmostIP bool `form:"useLeftmostIP" json:"useLeftmostIP"`
@@ -560,13 +561,17 @@ type RateLimitsConfig struct {
}
// FindRateLimitRule returns the first matching rule based on the provided labels.
func (c *RateLimitsConfig) FindRateLimitRule(searchLabels []string) (RateLimitRule, bool) {
//
// Optionally you can further specify a list of valid RateLimitRule.Audience values to further filter the matching rule
// (aka. the rule Audience will have to exist in one of the specified options).
func (c *RateLimitsConfig) FindRateLimitRule(searchLabels []string, optOnlyAudience ...string) (RateLimitRule, bool) {
var prefixRules []int
for i, label := range searchLabels {
// check for direct match
for j := range c.Rules {
if label == c.Rules[j].Label {
if label == c.Rules[j].Label &&
(len(optOnlyAudience) == 0 || slices.Contains(optOnlyAudience, c.Rules[j].Audience)) {
return c.Rules[j], true
}
@@ -578,7 +583,8 @@ func (c *RateLimitsConfig) FindRateLimitRule(searchLabels []string) (RateLimitRu
// check for prefix match
if len(prefixRules) > 0 {
for j := range prefixRules {
if strings.HasPrefix(label+"/", c.Rules[prefixRules[j]].Label) {
if strings.HasPrefix(label+"/", c.Rules[prefixRules[j]].Label) &&
(len(optOnlyAudience) == 0 || slices.Contains(optOnlyAudience, c.Rules[prefixRules[j]].Audience)) {
return c.Rules[prefixRules[j]], true
}
}
@@ -633,7 +639,7 @@ func checkUniqueRuleLabel(value any) error {
if conflicts {
return validation.Errors{
strconv.Itoa(i): validation.Errors{
"label": validation.NewError("validation_conflicting_rate_limit_rule", "Rate limit rule configuration with label "+rule.Label+" already exists or conflicts with another rule.").
"label": validation.NewError("validation_conflicting_rate_limit_rule", "Rate limit rule configuration with label {{.label}} already exists or conflicts with another rule.").
SetParams(map[string]any{"label": rule.Label}),
},
}
+26 -16
View File
@@ -634,33 +634,43 @@ func TestRateLimitsFindRateLimitRule(t *testing.T) {
limits := core.RateLimitsConfig{
Rules: []core.RateLimitRule{
{Label: "abc"},
{Label: "POST /test/a/"},
{Label: "/test/a/"},
{Label: "def", Audience: core.RateLimitRuleAudienceGuest},
{Label: "/test/a", Audience: core.RateLimitRuleAudienceGuest},
{Label: "POST /test/a"},
{Label: "/test/a"},
{Label: "/test/a/", Audience: core.RateLimitRuleAudienceAuth},
{Label: "POST /test/a/"},
},
}
scenarios := []struct {
labels []string
audience []string
expected string
}{
{[]string{}, ""},
{[]string{"missing"}, ""},
{[]string{"abc"}, "abc"},
{[]string{"/test"}, ""},
{[]string{"/test/a"}, "/test/a"},
{[]string{"GET /test/a"}, ""},
{[]string{"POST /test/a"}, "POST /test/a"},
{[]string{"/test/a/b/c"}, "/test/a/"},
{[]string{"GET /test/a/b/c"}, ""},
{[]string{"POST /test/a/b/c"}, "POST /test/a/"},
{[]string{"/test/a", "abc"}, "/test/a"}, // priority checks
{[]string{}, []string{}, ""},
{[]string{"missing"}, []string{}, ""},
{[]string{"abc"}, []string{}, "abc"},
{[]string{"abc"}, []string{core.RateLimitRuleAudienceGuest}, ""},
{[]string{"abc"}, []string{core.RateLimitRuleAudienceAuth}, ""},
{[]string{"def"}, []string{core.RateLimitRuleAudienceGuest}, "def"},
{[]string{"def"}, []string{core.RateLimitRuleAudienceAuth}, ""},
{[]string{"/test"}, []string{}, ""},
{[]string{"/test/a"}, []string{}, "/test/a"},
{[]string{"/test/a"}, []string{core.RateLimitRuleAudienceAuth}, "/test/a/"},
{[]string{"/test/a"}, []string{core.RateLimitRuleAudienceGuest}, "/test/a"},
{[]string{"GET /test/a"}, []string{}, ""},
{[]string{"POST /test/a"}, []string{}, "POST /test/a"},
{[]string{"/test/a/b/c"}, []string{}, "/test/a/"},
{[]string{"/test/a/b/c"}, []string{core.RateLimitRuleAudienceAuth}, "/test/a/"},
{[]string{"/test/a/b/c"}, []string{core.RateLimitRuleAudienceGuest}, ""},
{[]string{"GET /test/a/b/c"}, []string{}, ""},
{[]string{"POST /test/a/b/c"}, []string{}, "POST /test/a/"},
{[]string{"/test/a", "abc"}, []string{}, "/test/a"}, // priority checks
}
for _, s := range scenarios {
t.Run(strings.Join(s.labels, ""), func(t *testing.T) {
rule, ok := limits.FindRateLimitRule(s.labels)
t.Run(strings.Join(s.labels, "_")+":"+strings.Join(s.audience, "_"), func(t *testing.T) {
rule, ok := limits.FindRateLimitRule(s.labels, s.audience...)
hasLabel := rule.Label != ""
if hasLabel != ok {
+1 -1
View File
@@ -29,7 +29,7 @@ func UploadedFileSize(maxBytes int64) validation.RuleFunc {
if v.Size > maxBytes {
return validation.NewError(
"validation_file_size_limit",
fmt.Sprintf("Failed to upload %q - the maximum allowed file size is %v bytes.", v.OriginalName, maxBytes),
"Failed to upload {{.file}} - the maximum allowed file size is {{.maxSize}} bytes.",
).SetParams(map[string]any{
"file": v.OriginalName,
"maxSize": maxBytes,
+1
View File
@@ -247,6 +247,7 @@ func parseQueryToFields(app App, selectQuery string) (map[string]*queryField, er
System: true,
Required: true,
PrimaryKey: true,
Pattern: `^[a-z0-9]+$`,
},
}
continue
+3 -3
View File
@@ -193,9 +193,9 @@ func (form *RecordUpsert) validateFormFields() error {
}
func (form *RecordUpsert) checkOldPassword(value any) error {
v, _ := value.(string)
if v == "" || form.record.IsNew() {
return nil // nothing to check
v, ok := value.(string)
if !ok {
return validators.ErrUnsupportedValueType
}
if !form.record.Original().ValidatePassword(v) {
+1 -1
View File
@@ -107,7 +107,7 @@ func (form *TestEmailSend) Submit() error {
case TestTemplateEmailChange:
return mails.SendRecordChangeEmail(form.app, record, form.Email)
case TestTemplateOTP:
return mails.SendRecordOTP(form.app, record, "OTP_ID", "123456")
return mails.SendRecordOTP(form.app, record, "_PB_TEST_OTP_ID_", "123456")
case TestTemplateAuthAlert:
return mails.SendRecordAuthAlert(form.app, record)
default:
+38 -38
View File
@@ -4,53 +4,54 @@ go 1.23
require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/aws/aws-sdk-go-v2 v1.32.4
github.com/aws/aws-sdk-go-v2/config v1.28.3
github.com/aws/aws-sdk-go-v2/credentials v1.17.44
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.37
github.com/aws/aws-sdk-go-v2/service/s3 v1.66.3
github.com/aws/smithy-go v1.22.0
github.com/aws/aws-sdk-go-v2 v1.32.6
github.com/aws/aws-sdk-go-v2/config v1.28.6
github.com/aws/aws-sdk-go-v2/credentials v1.17.47
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.43
github.com/aws/aws-sdk-go-v2/service/s3 v1.71.0
github.com/aws/smithy-go v1.22.1
github.com/disintegration/imaging v1.6.2
github.com/domodwyer/mailyak/v3 v3.6.2
github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
github.com/fatih/color v1.18.0
github.com/fsnotify/fsnotify v1.7.0
github.com/gabriel-vasile/mimetype v1.4.6
github.com/gabriel-vasile/mimetype v1.4.7
github.com/ganigeorgiev/fexpr v0.4.1
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
github.com/golang-jwt/jwt/v4 v4.5.1
github.com/pocketbase/dbx v1.10.1
github.com/pocketbase/dbx v1.11.0
github.com/pocketbase/tygoja v0.0.0-20241015175937-d6ff411a0f75
github.com/spf13/cast v1.7.0
github.com/spf13/cast v1.7.1
github.com/spf13/cobra v1.8.1
gocloud.dev v0.40.0
golang.org/x/crypto v0.29.0
golang.org/x/net v0.31.0
golang.org/x/crypto v0.31.0
golang.org/x/net v0.32.0
golang.org/x/oauth2 v0.24.0
golang.org/x/sync v0.9.0
modernc.org/sqlite v1.33.1
golang.org/x/sync v0.10.0
modernc.org/sqlite v1.34.3
)
require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.25 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.7 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/gax-go/v2 v2.14.0 // indirect
@@ -65,20 +66,19 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.8.2 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
golang.org/x/image v0.22.0 // indirect
golang.org/x/image v0.23.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/term v0.26.0 // indirect
golang.org/x/text v0.20.0 // indirect
golang.org/x/tools v0.27.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/tools v0.28.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/api v0.205.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241113154021-e0fbfb71d213 // indirect
google.golang.org/grpc v1.68.0 // indirect
google.golang.org/protobuf v1.35.1 // indirect
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 // indirect
modernc.org/libc v1.61.0 // indirect
google.golang.org/api v0.213.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 // indirect
google.golang.org/grpc v1.69.0 // indirect
google.golang.org/protobuf v1.36.0 // indirect
modernc.org/gc/v3 v3.0.0-20241213165251-3bc300f6d0c9 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
+102 -98
View File
@@ -1,12 +1,12 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
cloud.google.com/go/auth v0.10.1 h1:TnK46qldSfHWt2a0b/hciaiVJsmDXWy9FqyUan0uYiI=
cloud.google.com/go/auth v0.10.1/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI=
cloud.google.com/go/auth/oauth2adapt v0.2.5 h1:2p29+dePqsCHPP1bqDJcKj4qxRyYCcbzKpFyKGt3MTk=
cloud.google.com/go/auth/oauth2adapt v0.2.5/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs=
cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q=
cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU=
cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/iam v1.1.13 h1:7zWBXG9ERbMLrzQBRhFliAV+kjcRToDTgQT3CTwYyv4=
cloud.google.com/go/iam v1.1.13/go.mod h1:K8mY0uSXwEXS30KrnVb+j54LB/ntfZu1dr+4zFMNbus=
cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=
@@ -25,44 +25,44 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.32.4 h1:S13INUiTxgrPueTmrm5DZ+MiAo99zYzHEFh1UNkOxNE=
github.com/aws/aws-sdk-go-v2 v1.32.4/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 h1:pT3hpW0cOHRJx8Y0DfJUEQuqPild8jRGmSFmBgvydr0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6/go.mod h1:j/I2++U0xX+cr44QjHay4Cvxj6FUbnxrgmqN3H1jTZA=
github.com/aws/aws-sdk-go-v2/config v1.28.3 h1:kL5uAptPcPKaJ4q0sDUjUIdueO18Q7JDzl64GpVwdOM=
github.com/aws/aws-sdk-go-v2/config v1.28.3/go.mod h1:SPEn1KA8YbgQnwiJ/OISU4fz7+F6Fe309Jf0QTsRCl4=
github.com/aws/aws-sdk-go-v2/credentials v1.17.44 h1:qqfs5kulLUHUEXlHEZXLJkgGoF3kkUeFUTVA585cFpU=
github.com/aws/aws-sdk-go-v2/credentials v1.17.44/go.mod h1:0Lm2YJ8etJdEdw23s+q/9wTpOeo2HhNE97XcRa7T8MA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 h1:woXadbf0c7enQ2UGCi8gW/WuKmE0xIzxBF/eD94jMKQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19/go.mod h1:zminj5ucw7w0r65bP6nhyOd3xL6veAUMc3ElGMoLVb4=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.37 h1:jHKR76E81sZvz1+x1vYYrHMxphG5LFBJPhSqEr4CLlE=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.37/go.mod h1:iMkyPkmoJWQKzSOtaX+8oEJxAuqr7s8laxcqGDSHeII=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 h1:A2w6m6Tmr+BNXjDsr7M90zkWjsu4JXHwrzPg235STs4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23/go.mod h1:35EVp9wyeANdujZruvHiQUAo9E3vbhnIO1mTCAxMlY0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 h1:pgYW9FCabt2M25MoHYCfMrVY2ghiiBKYWUVXfwZs+sU=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23/go.mod h1:c48kLgzO19wAu3CPkDWC28JbaJ+hfQlsdl7I2+oqIbk=
github.com/aws/aws-sdk-go-v2 v1.32.6 h1:7BokKRgRPuGmKkFMhEg/jSul+tB9VvXhcViILtfG8b4=
github.com/aws/aws-sdk-go-v2 v1.32.6/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc=
github.com/aws/aws-sdk-go-v2/config v1.28.6 h1:D89IKtGrs/I3QXOLNTH93NJYtDhm8SYa9Q5CsPShmyo=
github.com/aws/aws-sdk-go-v2/config v1.28.6/go.mod h1:GDzxJ5wyyFSCoLkS+UhGB0dArhb9mI+Co4dHtoTxbko=
github.com/aws/aws-sdk-go-v2/credentials v1.17.47 h1:48bA+3/fCdi2yAwVt+3COvmatZ6jUDNkDTIsqDiMUdw=
github.com/aws/aws-sdk-go-v2/credentials v1.17.47/go.mod h1:+KdckOejLW3Ks3b0E3b5rHsr2f9yuORBum0WPnE5o5w=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21 h1:AmoU1pziydclFT/xRV+xXE/Vb8fttJCLRPv8oAkprc0=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21/go.mod h1:AjUdLYe4Tgs6kpH4Bv7uMZo7pottoyHMn4eTcIcneaY=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.43 h1:iLdpkYZ4cXIQMO7ud+cqMWR1xK5ESbt1rvN77tRi1BY=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.43/go.mod h1:OgbsKPAswXDd5kxnR4vZov69p3oYjbvUyIRBAAV0y9o=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 h1:s/fF4+yDQDoElYhfIVvSNyeCydfbuTKzhxSXDXCPasU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25/go.mod h1:IgPfDv5jqFIzQSNbUEMoitNooSMXjRSDkhXv8jiROvU=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 h1:ZntTCl5EsYnhN/IygQEUugpdwbhdkom9uHcbCftiGgA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25/go.mod h1:DBdPrgeocww+CSl1C8cEV8PN1mHMBhuCDLpXezyvWkE=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23 h1:1SZBDiRzzs3sNhOMVApyWPduWYGAX0imGy06XiBnCAM=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23/go.mod h1:i9TkxgbZmHVh2S0La6CAXtnyFhlCX/pJ0JsOvBAS6Mk=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4 h1:aaPpoG15S2qHkWm4KlEyF01zovK1nW4BBbyXuHNSE90=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4/go.mod h1:eD9gS2EARTKgGr/W5xwgY/ik9z/zqpW+m/xOQbVxrMk=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 h1:tHxQi/XHPK0ctd/wdOw0t7Xrc2OxcRCnVzv8lwWPu0c=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4/go.mod h1:4GQbF1vJzG60poZqWatZlhP31y8PGCCVTvIGPdaaYJ0=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4 h1:E5ZAVOmI2apR8ADb72Q63KqwwwdW1XcMeXIlrZ1Psjg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4/go.mod h1:wezzqVUOVVdk+2Z/JzQT4NxAU0NbhRe5W8pIE72jsWI=
github.com/aws/aws-sdk-go-v2/service/s3 v1.66.3 h1:neNOYJl72bHrz9ikAEED4VqWyND/Po0DnEx64RW6YM4=
github.com/aws/aws-sdk-go-v2/service/s3 v1.66.3/go.mod h1:TMhLIyRIyoGVlaEMAt+ITMbwskSTpcGsCPDq91/ihY0=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 h1:HJwZwRt2Z2Tdec+m+fPjvdmkq2s9Ra+VR0hjF7V2o40=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.5/go.mod h1:wrMCEwjFPms+V86TCQQeOxQF/If4vT44FGIOFiMC2ck=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 h1:zcx9LiGWZ6i6pjdcoE9oXAB6mUdeyC36Ia/QEiIvYdg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4/go.mod h1:Tp/ly1cTjRLGBBmNccFumbZ8oqpZlpdhFf80SrRh4is=
github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 h1:yDxvkz3/uOKfxnv8YhzOi9m+2OGIxF+on3KOISbK5IU=
github.com/aws/aws-sdk-go-v2/service/sts v1.32.4/go.mod h1:9XEUty5v5UAsMiFOBJrNibZgwCeOma73jgGwwhgffa8=
github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM=
github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.25 h1:r67ps7oHCYnflpgDy2LZU0MAQtQbYIOqNNnqGO6xQkE=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.25/go.mod h1:GrGY+Q4fIokYLtjCVB/aFfCVL6hhGUFl8inD18fDalE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.6 h1:HCpPsWqmYQieU7SS6E9HXfdAMSud0pteVXieJmcpIRI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.6/go.mod h1:ngUiVRCco++u+soRRVBIvBZxSMMvOVMXA4PJ36JLfSw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6 h1:50+XsN70RS7dwJ2CkVNXzj7U2L1HKP8nqTd3XWEXBN4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6/go.mod h1:WqgLmwY7so32kG01zD8CPTJWVWM+TzJoOVHwTg4aPug=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.6 h1:BbGDtTi0T1DYlmjBiCr/le3wzhA37O8QTC5/Ab8+EXk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.6/go.mod h1:hLMJt7Q8ePgViKupeymbqI0la+t9/iYFBjxQCFwuAwI=
github.com/aws/aws-sdk-go-v2/service/s3 v1.71.0 h1:nyuzXooUNJexRT0Oy0UQY6AhOzxPxhtt4DcBIHyCnmw=
github.com/aws/aws-sdk-go-v2/service/s3 v1.71.0/go.mod h1:sT/iQz8JK3u/5gZkT+Hmr7GzVZehUMkRZpOaAwYXeGY=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.7 h1:rLnYAfXQ3YAccocshIH5mzNNwZBkBo+bP6EhIxak6Hw=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.7/go.mod h1:ZHtuQJ6t9A/+YDuxOLnbryAmITtr8UysSny3qcyvJTc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6 h1:JnhTZR3PiYDNKlXy50/pNeix9aGMo6lLpXwJ1mw8MD4=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6/go.mod h1:URronUEGfXZN1VpdktPSD1EkAL9mfrV+2F4sjH38qOY=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 h1:s4074ZO1Hk8qv65GqNXqDjmkf4HSQqJukaLuuW0TpDA=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.2/go.mod h1:mVggCnIWoM09jP71Wh+ea7+5gAp53q+49wDFs1SW5z8=
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
@@ -78,6 +78,8 @@ github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yA
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 h1:16iT9CBDOniJwFGPI41MbUDfEk74hFaKTqudrX8kenY=
github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217/go.mod h1:eIb+f24U+eWQCIsj9D/ah+MD9UP+wdxuqzsdLD+mhGM=
github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 h1:MXsAuToxwsTn5BEEYm2DheqIiC4jWGmkEJ1uy+KFhvQ=
github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc h1:MKYt39yZJi0Z9xEeRmDX2L4ocE0ETKcHKw6MVL3R+co=
@@ -96,8 +98,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
github.com/ganigeorgiev/fexpr v0.4.1 h1:hpUgbUEEWIZhSDBtf4M9aUNfQQ0BZkGRaMePy7Gcx5k=
github.com/ganigeorgiev/fexpr v0.4.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
@@ -115,8 +117,8 @@ github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQg
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -179,8 +181,8 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pocketbase/dbx v1.10.1 h1:cw+vsyfCJD8YObOVeqb93YErnlxwYMkNZ4rwN0G0AaA=
github.com/pocketbase/dbx v1.10.1/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pocketbase/tygoja v0.0.0-20241015175937-d6ff411a0f75 h1:XSbmekxgmbI2uPrre/nkCz7y8VsV652TPb3hAYzPb74=
github.com/pocketbase/tygoja v0.0.0-20241015175937-d6ff411a0f75/go.mod h1:hKJWPGFqavk3cdTa47Qvs8g37lnfI57OYdVVbIqW5aE=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -189,8 +191,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -212,25 +214,27 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.5
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
gocloud.dev v0.40.0 h1:f8LgP+4WDqOG/RXoUcyLpeIAGOcAbZrZbDQCUee10ng=
gocloud.dev v0.40.0/go.mod h1:drz+VyYNBvrMTW0KZiBAYEdl8lbNZx+OQ7oQvdrFmSQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g=
golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@@ -247,8 +251,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
@@ -256,8 +260,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -269,21 +273,21 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -291,33 +295,33 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/api v0.205.0 h1:LFaxkAIpDb/GsrWV20dMMo5MR0h8UARTbn24LmD+0Pg=
google.golang.org/api v0.205.0/go.mod h1:NrK1EMqO8Xk6l6QwRAmrXXg2v6dzukhlOyvkYtnvUuc=
google.golang.org/api v0.213.0 h1:KmF6KaDyFqB417T68tMPbVmmwtIXs2VB60OJKIHB0xQ=
google.golang.org/api v0.213.0/go.mod h1:V0T5ZhNUUNpYAlL306gFZPFt5F5D/IeyLoktduYYnvQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU=
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4=
google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 h1:CT2Thj5AuPV9phrYMtzX11k+XkzMGfRAet42PmoTATM=
google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988/go.mod h1:7uvplUBj4RjHAxIZ//98LzOvrQ04JBkaixRmCMI29hc=
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g=
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241113154021-e0fbfb71d213 h1:L+WcQXqkyf5MX6g7AudgYEuJjmYbqSRkTmJqGfAPw+Y=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241113154021-e0fbfb71d213/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 h1:Z7FRVJPSMaHQxD0uXU8WdgFh8PseLM8Q8NzhnpMrBhQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/grpc v1.69.0 h1:quSiOM1GJPmPH5XtU+BCoVXcDVJJAzNcoyfC2cCjGkI=
google.golang.org/grpc v1.69.0/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -327,8 +331,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ=
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
@@ -340,16 +344,16 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 h1:IYXPPTTjjoSHvUClZIYexDiO7g+4x+XveKT4gCIAwiY=
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20241213165251-3bc300f6d0c9 h1:ovz6yUKX71igz2yvk4NpiCL5fvdjZAI+DhuDEGx1xyU=
modernc.org/gc/v3 v3.0.0-20241213165251-3bc300f6d0c9/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
@@ -358,8 +362,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
modernc.org/sqlite v1.34.3 h1:494MIwJKBLd0tErBYkRar2HvEpy04Bl0ykPEm4XLhbo=
modernc.org/sqlite v1.34.3/go.mod h1:dnR723UrTtjKpoHCAMN0Q/gZ9MT4r+iRvIBb9umWFkU=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+20 -3
View File
@@ -1,8 +1,10 @@
package mails
import (
"html"
"html/template"
"net/mail"
"slices"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/mails/templates"
@@ -89,8 +91,8 @@ func SendRecordOTP(app core.App, authRecord *core.Record, otpId string, pass str
otp, err := e.App.FindOTPById(otpId)
if err != nil {
e.App.Logger().Error(
"Failed to find OTP to update its sentTo field",
e.App.Logger().Warn(
"Unable to find OTP to update its sentTo field (either it was already deleted or the id is nonexisting)",
"error", err,
"otpId", otpId,
)
@@ -232,6 +234,13 @@ func SendRecordChangeEmail(app core.App, authRecord *core.Record, newEmail strin
})
}
var nonescapeTypes = []string{
core.FieldTypeAutodate,
core.FieldTypeDate,
core.FieldTypeBool,
core.FieldTypeNumber,
}
func resolveEmailTemplate(
app core.App,
authRecord *core.Record,
@@ -258,7 +267,15 @@ func resolveEmailTemplate(
fieldPlacehodler := "{RECORD:" + field.GetName() + "}"
if _, ok := placeholders[fieldPlacehodler]; !ok {
placeholders[fieldPlacehodler] = authRecord.Get(field.GetName())
val := authRecord.GetString(field.GetName())
// note: the escaping is not strictly necessary but for just in case
// the user decide to store and render the email as plain html
if !slices.Contains(nonescapeTypes, field.Type()) {
val = html.EscapeString(val)
}
placeholders[fieldPlacehodler] = val
}
}
+21 -5
View File
@@ -1,6 +1,7 @@
package mails_test
import (
"html"
"strings"
"testing"
@@ -16,6 +17,9 @@ func TestSendRecordAuthAlert(t *testing.T) {
user, _ := testApp.FindFirstRecordByData("users", "email", "test@example.com")
// to test that it is escaped
user.Set("name", "<p>"+user.GetString("name")+"</p>")
err := mails.SendRecordAuthAlert(testApp, user)
if err != nil {
t.Fatal(err)
@@ -26,7 +30,7 @@ func TestSendRecordAuthAlert(t *testing.T) {
}
expectedParts := []string{
user.GetString("name") + "{RECORD:tokenKey}", // public and private record placeholder checks
html.EscapeString(user.GetString("name")) + "{RECORD:tokenKey}", // public and private record placeholder checks
"login to your " + testApp.Settings().Meta.AppName + " account from a new location",
"If this was you",
"If this wasn't you",
@@ -46,6 +50,9 @@ func TestSendRecordPasswordReset(t *testing.T) {
user, _ := testApp.FindFirstRecordByData("users", "email", "test@example.com")
// to test that it is escaped
user.Set("name", "<p>"+user.GetString("name")+"</p>")
err := mails.SendRecordPasswordReset(testApp, user)
if err != nil {
t.Fatal(err)
@@ -56,7 +63,7 @@ func TestSendRecordPasswordReset(t *testing.T) {
}
expectedParts := []string{
user.GetString("name") + "{RECORD:tokenKey}", // the record name as {RECORD:name}
html.EscapeString(user.GetString("name")) + "{RECORD:tokenKey}", // the record name as {RECORD:name}
"http://localhost:8090/_/#/auth/confirm-password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
}
for _, part := range expectedParts {
@@ -74,6 +81,9 @@ func TestSendRecordVerification(t *testing.T) {
user, _ := testApp.FindFirstRecordByData("users", "email", "test@example.com")
// to test that it is escaped
user.Set("name", "<p>"+user.GetString("name")+"</p>")
err := mails.SendRecordVerification(testApp, user)
if err != nil {
t.Fatal(err)
@@ -84,7 +94,7 @@ func TestSendRecordVerification(t *testing.T) {
}
expectedParts := []string{
user.GetString("name") + "{RECORD:tokenKey}", // the record name as {RECORD:name}
html.EscapeString(user.GetString("name")) + "{RECORD:tokenKey}", // the record name as {RECORD:name}
"http://localhost:8090/_/#/auth/confirm-verification/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
}
for _, part := range expectedParts {
@@ -102,6 +112,9 @@ func TestSendRecordChangeEmail(t *testing.T) {
user, _ := testApp.FindFirstRecordByData("users", "email", "test@example.com")
// to test that it is escaped
user.Set("name", "<p>"+user.GetString("name")+"</p>")
err := mails.SendRecordChangeEmail(testApp, user, "new_test@example.com")
if err != nil {
t.Fatal(err)
@@ -112,7 +125,7 @@ func TestSendRecordChangeEmail(t *testing.T) {
}
expectedParts := []string{
user.GetString("name") + "{RECORD:tokenKey}", // the record name as {RECORD:name}
html.EscapeString(user.GetString("name")) + "{RECORD:tokenKey}", // the record name as {RECORD:name}
"http://localhost:8090/_/#/auth/confirm-email-change/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
}
for _, part := range expectedParts {
@@ -130,6 +143,9 @@ func TestSendRecordOTP(t *testing.T) {
user, _ := testApp.FindFirstRecordByData("users", "email", "test@example.com")
// to test that it is escaped
user.Set("name", "<p>"+user.GetString("name")+"</p>")
err := mails.SendRecordOTP(testApp, user, "test_otp_id", "test_otp_code")
if err != nil {
t.Fatal(err)
@@ -140,7 +156,7 @@ func TestSendRecordOTP(t *testing.T) {
}
expectedParts := []string{
user.GetString("name") + "{RECORD:tokenKey}", // the record name as {RECORD:name}
html.EscapeString(user.GetString("name")) + "{RECORD:tokenKey}", // the record name as {RECORD:name}
"one-time password",
"test_otp_code",
}
+15 -13
View File
@@ -365,8 +365,10 @@ func migrateOldCollections(txApp core.App, oldSettings *oldSettingsModel) error
c.Options["manageRule"] = nil
if options["manageRule"] != nil {
manageRule := cast.ToString(options["manageRule"])
c.Options["manageRule"] = &manageRule
manageRule, err := cast.ToStringE(options["manageRule"])
if err == nil && manageRule != "" {
c.Options["manageRule"] = migrateRule(&manageRule)
}
}
// passwordAuth
@@ -494,15 +496,15 @@ func migrateOldCollections(txApp core.App, oldSettings *oldSettingsModel) error
// ---
c.Indexes = append(types.JSONArray[string]{
fmt.Sprintf("CREATE UNIQUE INDEX `_%s_username_idx` ON `%s` (username COLLATE NOCASE)", c.Id, c.Name),
fmt.Sprintf("CREATE UNIQUE INDEX `_%s_email_idx` ON `%s` (email) WHERE email != ''", c.Id, c.Name),
fmt.Sprintf("CREATE UNIQUE INDEX `_%s_tokenKey_idx` ON `%s` (tokenKey)", c.Id, c.Name),
fmt.Sprintf("CREATE UNIQUE INDEX `_%s_email_idx` ON `%s` (`email`) WHERE `email` != ''", c.Id, c.Name),
fmt.Sprintf("CREATE UNIQUE INDEX `_%s_tokenKey_idx` ON `%s` (`tokenKey`)", c.Id, c.Name),
}, c.Indexes...)
// prepend the auth system fields
// ---
tokenKeyField := map[string]any{
"id": fieldIdChecksum("text", "tokenKey"),
"type": "text",
"id": "_pbf_auth_tokenKey_",
"name": "tokenKey",
"system": true,
"hidden": true,
@@ -515,8 +517,8 @@ func migrateOldCollections(txApp core.App, oldSettings *oldSettingsModel) error
"autogeneratePattern": "[a-zA-Z0-9_]{50}",
}
passwordField := map[string]any{
"id": fieldIdChecksum("password", "password"),
"type": "password",
"id": "_pbf_auth_password_",
"name": "password",
"presentable": false,
"system": true,
@@ -527,8 +529,8 @@ func migrateOldCollections(txApp core.App, oldSettings *oldSettingsModel) error
"cost": bcrypt.DefaultCost, // new default
}
emailField := map[string]any{
"id": fieldIdChecksum("email", "email"),
"type": "email",
"id": "_pbf_auth_email_",
"name": "email",
"system": true,
"hidden": false,
@@ -538,8 +540,8 @@ func migrateOldCollections(txApp core.App, oldSettings *oldSettingsModel) error
"onlyDomains": cast.ToStringSlice(options["onlyEmailDomains"]),
}
emailVisibilityField := map[string]any{
"id": fieldIdChecksum("bool", "emailVisibility"),
"type": "bool",
"id": "_pbf_auth_emailVisibility_",
"name": "emailVisibility",
"system": true,
"hidden": false,
@@ -547,8 +549,8 @@ func migrateOldCollections(txApp core.App, oldSettings *oldSettingsModel) error
"required": false,
}
verifiedField := map[string]any{
"id": fieldIdChecksum("bool", "verified"),
"type": "bool",
"id": "_pbf_auth_verified_",
"name": "verified",
"system": true,
"hidden": false,
@@ -556,8 +558,8 @@ func migrateOldCollections(txApp core.App, oldSettings *oldSettingsModel) error
"required": false,
}
usernameField := map[string]any{
"id": fieldIdChecksum("text", "username"),
"type": "text",
"id": "_pbf_auth_username_",
"name": "username",
"system": false,
"hidden": false,
@@ -597,8 +599,8 @@ func migrateOldCollections(txApp core.App, oldSettings *oldSettingsModel) error
// prepend the id field
idField := map[string]any{
"id": fieldIdChecksum("text", "id"),
"type": "text",
"id": "_pbf_text_id_",
"name": "id",
"system": true,
"required": true,
@@ -631,8 +633,8 @@ func migrateOldCollections(txApp core.App, oldSettings *oldSettingsModel) error
if addCreated {
createdField := map[string]any{
"id": fieldIdChecksum("autodate", "created"),
"type": "autodate",
"id": "_pbf_autodate_created_",
"name": "created",
"system": false,
"presentable": false,
@@ -645,8 +647,8 @@ func migrateOldCollections(txApp core.App, oldSettings *oldSettingsModel) error
if addUpdated {
updatedField := map[string]any{
"id": fieldIdChecksum("autodate", "updated"),
"type": "autodate",
"id": "_pbf_autodate_updated_",
"name": "updated",
"system": false,
"presentable": false,
+6 -6
View File
@@ -10,12 +10,12 @@ import (
// note: this migration will be deleted in future version
func collectionIdChecksum(c *core.Collection) string {
return "pbc_" + strconv.Itoa(int(crc32.ChecksumIEEE([]byte(c.Type+c.Name))))
func collectionIdChecksum(typ, name string) string {
return "pbc_" + strconv.FormatInt(int64(crc32.ChecksumIEEE([]byte(typ+name))), 10)
}
func fieldIdChecksum(f core.Field) string {
return f.Type() + strconv.Itoa(int(crc32.ChecksumIEEE([]byte(f.GetName()))))
func fieldIdChecksum(typ, name string) string {
return typ + strconv.FormatInt(int64(crc32.ChecksumIEEE([]byte(name))), 10)
}
// normalize system collection and field ids
@@ -62,7 +62,7 @@ func init() {
originalId := c.Id
// normalize collection id
if checksum := collectionIdChecksum(c); c.Id != checksum {
if checksum := collectionIdChecksum(c.Type, c.Name); c.Id != checksum {
c.Id = checksum
needUpdate = true
}
@@ -73,7 +73,7 @@ func init() {
continue
}
if checksum := fieldIdChecksum(f); f.GetId() != checksum {
if checksum := fieldIdChecksum(f.Type(), f.GetName()); f.GetId() != checksum {
f.SetId(checksum)
needUpdate = true
}
+35 -9
View File
@@ -81,14 +81,14 @@ func hooksBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
res, err := executor.RunProgram(pr)
executor.Set("__args", goja.Undefined())
// check for returned error value
// (legacy) check for returned Go error value
if res != nil {
if resErr, ok := res.Export().(error); ok {
return resErr
}
}
return err
return normalizeException(err)
})
return []reflect.Value{reflect.ValueOf(&err).Elem()}
@@ -197,14 +197,14 @@ func wrapHandlerFunc(executors *vmsPool, handler goja.Value) (func(*core.Request
res, err := executor.RunProgram(pr)
executor.Set("__args", goja.Undefined())
// check for returned error
// (legacy) check for returned Go error value
if res != nil {
if v, ok := res.Export().(error); ok {
return v
}
}
return err
return normalizeException(err)
})
}
@@ -215,9 +215,9 @@ func wrapHandlerFunc(executors *vmsPool, handler goja.Value) (func(*core.Request
}
type gojaHookHandler struct {
priority int
id string
serializedFunc string
priority int
}
func wrapMiddlewares(executors *vmsPool, rawMiddlewares ...goja.Value) ([]*hook.Handler[*core.RequestEvent], error) {
@@ -254,14 +254,14 @@ func wrapMiddlewares(executors *vmsPool, rawMiddlewares ...goja.Value) ([]*hook.
res, err := executor.RunProgram(pr)
executor.Set("__args", goja.Undefined())
// check for returned error
// (legacy) check for returned Go error value
if res != nil {
if v, ok := res.Export().(error); ok {
return v
}
}
return err
return normalizeException(err)
})
},
}
@@ -276,14 +276,14 @@ func wrapMiddlewares(executors *vmsPool, rawMiddlewares ...goja.Value) ([]*hook.
res, err := executor.RunProgram(pr)
executor.Set("__args", goja.Undefined())
// check for returned error
// (legacy) check for returned Go error value
if res != nil {
if v, ok := res.Export().(error); ok {
return v
}
}
return err
return normalizeException(err)
})
},
}
@@ -1019,3 +1019,29 @@ func newDynamicModel(shape map[string]any) any {
return elem.Addr().Interface()
}
// normalizeException checks if the provided error is a goja.Exception
// and attempts to return its underlying Go error.
//
// note: using just goja.Exception.Unwrap() is insufficient and may falsely result in nil.
func normalizeException(err error) error {
if err == nil {
return nil
}
jsException, ok := err.(*goja.Exception)
if !ok {
return err // no exception
}
switch v := jsException.Value().Export().(type) {
case error:
err = v
case map[string]any: // goja.GoError
if vErr, ok := v["value"].(error); ok {
err = vErr
}
}
return err
}
+42
View File
@@ -2,6 +2,7 @@ package jsvm
import (
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
@@ -1456,6 +1457,47 @@ func TestHooksBinds(t *testing.T) {
}
}
func TestHooksExceptionUnwrapping(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
goErr := errors.New("test")
vmFactory := func() *goja.Runtime {
vm := goja.New()
baseBinds(vm)
vm.Set("$app", app)
vm.Set("goErr", goErr)
return vm
}
pool := newPool(1, vmFactory)
vm := vmFactory()
hooksBinds(app, vm, pool)
_, err := vm.RunString(`
onModelUpdate((e) => {
throw goErr
}, "demo1")
`)
if err != nil {
t.Fatal(err)
}
record, err := app.FindFirstRecordByFilter("demo1", "1=1")
if err != nil {
t.Fatal(err)
}
record.Set("text", "update")
err = app.Save(record)
if !errors.Is(err, goErr) {
t.Fatalf("Expected goError, got %v", err)
}
}
func TestRouterBindsCount(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
File diff suppressed because it is too large Load Diff
+16 -5
View File
@@ -771,20 +771,20 @@ declare namespace $filesystem {
let fileFromMultipart: filesystem.newFileFromMultipart
/**
* fileFromUrl creates a new File from the provided url by
* fileFromURL creates a new File from the provided url by
* downloading the resource and creating a BytesReader.
*
* Example:
*
* ` + "```" + `js
* // with default max timeout of 120sec
* const file1 = $filesystem.fileFromUrl("https://...")
* const file1 = $filesystem.fileFromURL("https://...")
*
* // with custom timeout of 15sec
* const file2 = $filesystem.fileFromUrl("https://...", 15)
* const file2 = $filesystem.fileFromURL("https://...", 15)
* ` + "```" + `
*/
export function fileFromUrl(url: string, secTimeout?: number): filesystem.File
export function fileFromURL(url: string, secTimeout?: number): filesystem.File
}
// -------------------------------------------------------------------
@@ -1002,9 +1002,20 @@ declare namespace $apis {
let skipSuccessActivityLog: apis.skipSuccessActivityLog
let gzip: apis.gzip
let bodyLimit: apis.bodyLimit
let recordAuthResponse: apis.recordAuthResponse
let enrichRecord: apis.enrichRecord
let enrichRecords: apis.enrichRecords
/**
* RecordAuthResponse writes standardized json record auth response
* into the specified request event.
*
* The authMethod argument specify the name of the current authentication method (eg. password, oauth2, etc.)
* that it is used primarily as an auth identifier during MFA and for login alerts.
*
* Set authMethod to empty string if you want to ignore the MFA checks and the login alerts
* (can be also adjusted additionally via the onRecordAuthRequest hook).
*/
export function recordAuthResponse(e: core.RequestEvent, authRecord: core.Record, authMethod: string, meta?: any): void
}
// -------------------------------------------------------------------
+7 -16
View File
@@ -4,7 +4,7 @@
// Example:
//
// jsvm.MustRegister(app, jsvm.Config{
// WatchHooks: true,
// HooksWatch: true,
// })
package jsvm
@@ -22,6 +22,7 @@ import (
"time"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/buffer"
"github.com/dop251/goja_nodejs/console"
"github.com/dop251/goja_nodejs/process"
"github.com/dop251/goja_nodejs/require"
@@ -169,9 +170,12 @@ func (p *plugin) registerMigrations() error {
for file, content := range files {
vm := goja.New()
registry.Enable(vm)
console.Enable(vm)
process.Enable(vm)
buffer.Enable(vm)
baseBinds(vm)
dbxBinds(vm)
securityBinds(vm)
@@ -252,6 +256,7 @@ func (p *plugin) registerHooks() error {
requireRegistry.Enable(vm)
console.Enable(vm)
process.Enable(vm)
buffer.Enable(vm)
baseBinds(vm)
dbxBinds(vm)
@@ -321,21 +326,7 @@ func (p *plugin) normalizeServeExceptions(e *core.RequestEvent) error {
return err // no error or already committed
}
jsException, ok := err.(*goja.Exception)
if !ok {
return err // no exception
}
switch v := jsException.Value().Export().(type) {
case error:
err = v
case map[string]any: // goja.GoError
if vErr, ok := v["value"].(error); ok {
err = vErr
}
}
return err
return normalizeException(err)
}
// watchHooks initializes a hooks file watcher that will restart the
+8 -8
View File
@@ -921,7 +921,7 @@ migrate((app) => {
collection.fields.removeById("f3_id")
// add field
collection.fields.add(new Field({
collection.fields.addAt(8, new Field({
"autogeneratePattern": "",
"hidden": false,
"id": "f4_id",
@@ -937,7 +937,7 @@ migrate((app) => {
}))
// update field
collection.fields.add(new Field({
collection.fields.addAt(7, new Field({
"hidden": false,
"id": "f2_id",
"max": null,
@@ -975,7 +975,7 @@ migrate((app) => {
}, collection)
// add field
collection.fields.add(new Field({
collection.fields.addAt(8, new Field({
"hidden": false,
"id": "f3_id",
"name": "f3_name",
@@ -989,7 +989,7 @@ migrate((app) => {
collection.fields.removeById("f4_id")
// update field
collection.fields.add(new Field({
collection.fields.addAt(7, new Field({
"hidden": false,
"id": "f2_id",
"max": null,
@@ -1052,7 +1052,7 @@ func init() {
collection.Fields.RemoveById("f3_id")
// add field
if err := collection.Fields.AddMarshaledJSON([]byte(` + "`" + `{
if err := collection.Fields.AddMarshaledJSONAt(8, []byte(` + "`" + `{
"autogeneratePattern": "",
"hidden": false,
"id": "f4_id",
@@ -1070,7 +1070,7 @@ func init() {
}
// update field
if err := collection.Fields.AddMarshaledJSON([]byte(` + "`" + `{
if err := collection.Fields.AddMarshaledJSONAt(7, []byte(` + "`" + `{
"hidden": false,
"id": "f2_id",
"max": null,
@@ -1115,7 +1115,7 @@ func init() {
}
// add field
if err := collection.Fields.AddMarshaledJSON([]byte(` + "`" + `{
if err := collection.Fields.AddMarshaledJSONAt(8, []byte(` + "`" + `{
"hidden": false,
"id": "f3_id",
"name": "f3_name",
@@ -1131,7 +1131,7 @@ func init() {
collection.Fields.RemoveById("f4_id")
// update field
if err := collection.Fields.AddMarshaledJSON([]byte(` + "`" + `{
if err := collection.Fields.AddMarshaledJSONAt(7, []byte(` + "`" + `{
"hidden": false,
"id": "f2_id",
"max": null,
+16 -8
View File
@@ -208,7 +208,7 @@ func (p *plugin) jsDiffTemplate(new *core.Collection, old *core.Collection) (str
upParts = append(upParts, fmt.Sprintf("%s.fields.removeById(%q)\n", varName, oldField.GetId()))
downParts = append(downParts, "// add field")
downParts = append(downParts, fmt.Sprintf("%s.fields.add(new Field(%s))\n", varName, rawOldField))
downParts = append(downParts, fmt.Sprintf("%s.fields.addAt(%d, new Field(%s))\n", varName, i, rawOldField))
}
// created fields
@@ -223,13 +223,14 @@ func (p *plugin) jsDiffTemplate(new *core.Collection, old *core.Collection) (str
}
upParts = append(upParts, "// add field")
upParts = append(upParts, fmt.Sprintf("%s.fields.add(new Field(%s))\n", varName, rawNewField))
upParts = append(upParts, fmt.Sprintf("%s.fields.addAt(%d, new Field(%s))\n", varName, i, rawNewField))
downParts = append(downParts, "// remove field")
downParts = append(downParts, fmt.Sprintf("%s.fields.removeById(%q)\n", varName, newField.GetId()))
}
// modified fields
// (note currently ignoring order-only changes as it comes with too many edge-cases)
for i, newField := range new.Fields {
var rawNewField, rawOldField []byte
@@ -238,12 +239,15 @@ func (p *plugin) jsDiffTemplate(new *core.Collection, old *core.Collection) (str
return "", err
}
var oldFieldIndex int
for j, oldField := range old.Fields {
if oldField.GetId() == newField.GetId() {
rawOldField, err = marhshalWithoutEscape(oldFieldsSlice[j], " ", " ")
if err != nil {
return "", err
}
oldFieldIndex = j
break
}
}
@@ -253,10 +257,10 @@ func (p *plugin) jsDiffTemplate(new *core.Collection, old *core.Collection) (str
}
upParts = append(upParts, "// update field")
upParts = append(upParts, fmt.Sprintf("%s.fields.add(new Field(%s))\n", varName, rawNewField))
upParts = append(upParts, fmt.Sprintf("%s.fields.addAt(%d, new Field(%s))\n", varName, i, rawNewField))
downParts = append(downParts, "// update field")
downParts = append(downParts, fmt.Sprintf("%s.fields.add(new Field(%s))\n", varName, rawOldField))
downParts = append(downParts, fmt.Sprintf("%s.fields.addAt(%d, new Field(%s))\n", varName, oldFieldIndex, rawOldField))
}
// -----------------------------------------------------------------
@@ -546,7 +550,7 @@ func (p *plugin) goDiffTemplate(new *core.Collection, old *core.Collection) (str
upParts = append(upParts, fmt.Sprintf("%s.Fields.RemoveById(%q)\n", varName, oldField.GetId()))
downParts = append(downParts, "// add field")
downParts = append(downParts, goErrIf(fmt.Sprintf("%s.Fields.AddMarshaledJSON([]byte(`%s`))", varName, escapeBacktick(string(rawOldField)))))
downParts = append(downParts, goErrIf(fmt.Sprintf("%s.Fields.AddMarshaledJSONAt(%d, []byte(`%s`))", varName, i, escapeBacktick(string(rawOldField)))))
}
// created fields
@@ -561,13 +565,14 @@ func (p *plugin) goDiffTemplate(new *core.Collection, old *core.Collection) (str
}
upParts = append(upParts, "// add field")
upParts = append(upParts, goErrIf(fmt.Sprintf("%s.Fields.AddMarshaledJSON([]byte(`%s`))", varName, escapeBacktick(string(rawNewField)))))
upParts = append(upParts, goErrIf(fmt.Sprintf("%s.Fields.AddMarshaledJSONAt(%d, []byte(`%s`))", varName, i, escapeBacktick(string(rawNewField)))))
downParts = append(downParts, "// remove field")
downParts = append(downParts, fmt.Sprintf("%s.Fields.RemoveById(%q)\n", varName, newField.GetId()))
}
// modified fields
// (note currently ignoring order-only changes as it comes with too many edge-cases)
for i, newField := range new.Fields {
var rawNewField, rawOldField []byte
@@ -576,12 +581,15 @@ func (p *plugin) goDiffTemplate(new *core.Collection, old *core.Collection) (str
return "", err
}
var oldFieldIndex int
for j, oldField := range old.Fields {
if oldField.GetId() == newField.GetId() {
rawOldField, err = marhshalWithoutEscape(oldFieldsSlice[j], "\t\t", "\t")
if err != nil {
return "", err
}
oldFieldIndex = j
break
}
}
@@ -591,10 +599,10 @@ func (p *plugin) goDiffTemplate(new *core.Collection, old *core.Collection) (str
}
upParts = append(upParts, "// update field")
upParts = append(upParts, goErrIf(fmt.Sprintf("%s.Fields.AddMarshaledJSON([]byte(`%s`))", varName, escapeBacktick(string(rawNewField)))))
upParts = append(upParts, goErrIf(fmt.Sprintf("%s.Fields.AddMarshaledJSONAt(%d, []byte(`%s`))", varName, i, escapeBacktick(string(rawNewField)))))
downParts = append(downParts, "// update field")
downParts = append(downParts, goErrIf(fmt.Sprintf("%s.Fields.AddMarshaledJSON([]byte(`%s`))", varName, escapeBacktick(string(rawOldField)))))
downParts = append(downParts, goErrIf(fmt.Sprintf("%s.Fields.AddMarshaledJSONAt(%d, []byte(`%s`))", varName, oldFieldIndex, escapeBacktick(string(rawOldField)))))
}
// ---------------------------------------------------------------
+6 -6
View File
@@ -64,9 +64,9 @@ type Config struct {
//
// Note that the application will not be initialized/bootstrapped yet,
// aka. DB connections, migrations, app settings, etc. will not be accessible.
// Everything will be initialized when [PocketBase.Start()] is executed.
// If you want to initialize the application before calling [PocketBase.Start()],
// then you'll have to manually call [PocketBase.Bootstrap()].
// Everything will be initialized when [PocketBase.Start] is executed.
// If you want to initialize the application before calling [PocketBase.Start],
// then you'll have to manually call [PocketBase.Bootstrap].
func New() *PocketBase {
_, isUsingGoRun := inspectRuntime()
@@ -79,9 +79,9 @@ func New() *PocketBase {
//
// Note that the application will not be initialized/bootstrapped yet,
// aka. DB connections, migrations, app settings, etc. will not be accessible.
// Everything will be initialized when [PocketBase.Start()] is executed.
// If you want to initialize the application before calling [PocketBase..Start()],
// then you'll have to manually call [PocketBase.Bootstrap()].
// Everything will be initialized when [PocketBase.Start] is executed.
// If you want to initialize the application before calling [PocketBase.Start],
// then you'll have to manually call [PocketBase.Bootstrap].
func NewWithConfig(config Config) *PocketBase {
// initialize a default data directory based on the executable baseDir
if config.DefaultDataDir == "" {
Binary file not shown.
+10 -1
View File
@@ -7,7 +7,7 @@ import (
)
func TestProvidersCount(t *testing.T) {
expected := 28
expected := 29
if total := len(auth.Providers); total != expected {
t.Fatalf("Expected %d providers, got %d", expected, total)
@@ -278,4 +278,13 @@ func TestNewProviderByName(t *testing.T) {
if _, ok := p.(*auth.Wakatime); !ok {
t.Error("Expected to be instance of *auth.Wakatime")
}
// linear
p, err = auth.NewProviderByName(auth.NameLinear)
if err != nil {
t.Errorf("Expected nil, got error %v", err)
}
if _, ok := p.(*auth.Linear); !ok {
t.Error("Expected to be instance of *auth.Linear")
}
}
+2 -2
View File
@@ -51,18 +51,18 @@ func (p *Gitea) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
}
extracted := struct {
Id int `json:"id"`
Name string `json:"full_name"`
Username string `json:"login"`
Email string `json:"email"`
AvatarURL string `json:"avatar_url"`
Id int64 `json:"id"`
}{}
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
user := &AuthUser{
Id: strconv.Itoa(extracted.Id),
Id: strconv.FormatInt(extracted.Id, 10),
Name: extracted.Name,
Username: extracted.Username,
Email: extracted.Email,
+2 -2
View File
@@ -54,17 +54,17 @@ func (p *Gitee) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
extracted := struct {
Login string `json:"login"`
Id int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
AvatarURL string `json:"avatar_url"`
Id int64 `json:"id"`
}{}
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
user := &AuthUser{
Id: strconv.Itoa(extracted.Id),
Id: strconv.FormatInt(extracted.Id, 10),
Name: extracted.Name,
Username: extracted.Login,
AvatarURL: extracted.AvatarURL,
+2 -2
View File
@@ -54,17 +54,17 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
extracted := struct {
Login string `json:"login"`
Id int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
AvatarURL string `json:"avatar_url"`
Id int64 `json:"id"`
}{}
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
user := &AuthUser{
Id: strconv.Itoa(extracted.Id),
Id: strconv.FormatInt(extracted.Id, 10),
Name: extracted.Name,
Username: extracted.Login,
Email: extracted.Email,
+2 -2
View File
@@ -51,18 +51,18 @@ func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
}
extracted := struct {
Id int `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Email string `json:"email"`
AvatarURL string `json:"avatar_url"`
Id int64 `json:"id"`
}{}
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
user := &AuthUser{
Id: strconv.Itoa(extracted.Id),
Id: strconv.FormatInt(extracted.Id, 10),
Name: extracted.Name,
Username: extracted.Username,
Email: extracted.Email,
+2 -2
View File
@@ -52,7 +52,6 @@ func (p *Kakao) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
}
extracted := struct {
Id int `json:"id"`
Profile struct {
Nickname string `json:"nickname"`
ImageURL string `json:"profile_image"`
@@ -62,13 +61,14 @@ func (p *Kakao) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
IsEmailVerified bool `json:"is_email_verified"`
IsEmailValid bool `json:"is_email_valid"`
} `json:"kakao_account"`
Id int64 `json:"id"`
}{}
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
user := &AuthUser{
Id: strconv.Itoa(extracted.Id),
Id: strconv.FormatInt(extracted.Id, 10),
Username: extracted.Profile.Nickname,
AvatarURL: extracted.Profile.ImageURL,
RawUser: rawUser,
+109
View File
@@ -0,0 +1,109 @@
package auth
import (
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"github.com/pocketbase/pocketbase/tools/types"
"golang.org/x/oauth2"
)
func init() {
Providers[NameLinear] = wrapFactory(NewLinearProvider)
}
var _ Provider = (*Linear)(nil)
// NameLinear is the unique name of the Linear provider.
const NameLinear string = "linear"
// Linear allows authentication via Linear OAuth2.
type Linear struct {
BaseProvider
}
// NewLinearProvider creates new Linear provider instance with some defaults.
//
// API reference: https://developers.linear.app/docs/oauth/authentication
func NewLinearProvider() *Linear {
return &Linear{BaseProvider{
ctx: context.Background(),
displayName: "Linear",
pkce: false, // Linear doesn't support PKCE at the moment and returns an error if enabled
scopes: []string{"read"},
authURL: "https://linear.app/oauth/authorize",
tokenURL: "https://api.linear.app/oauth/token",
userInfoURL: "https://api.linear.app/graphql",
}}
}
// FetchAuthUser returns an AuthUser instance based on the Linear's user api.
//
// API reference: https://developers.linear.app/docs/graphql/working-with-the-graphql-api#authentication
func (p *Linear) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
data, err := p.FetchRawUserInfo(token)
if err != nil {
return nil, err
}
rawUser := map[string]any{}
if err := json.Unmarshal(data, &rawUser); err != nil {
return nil, err
}
extracted := struct {
Data struct {
Viewer struct {
Id string `json:"id"`
DisplayName string `json:"displayName"`
Name string `json:"name"`
Email string `json:"email"`
AvatarURL string `json:"avatarUrl"`
Active bool `json:"active"`
} `json:"viewer"`
} `json:"data"`
}{}
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
if !extracted.Data.Viewer.Active {
return nil, errors.New("the Linear user account is not active")
}
user := &AuthUser{
Id: extracted.Data.Viewer.Id,
Name: extracted.Data.Viewer.Name,
Username: extracted.Data.Viewer.DisplayName,
Email: extracted.Data.Viewer.Email,
AvatarURL: extracted.Data.Viewer.AvatarURL,
RawUser: rawUser,
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
}
user.Expiry, _ = types.ParseDateTime(token.Expiry)
return user, nil
}
// FetchRawUserInfo implements Provider.FetchRawUserInfo interface method.
//
// Linear doesn't have a UserInfo endpoint and information on the user
// is retrieved using their GraphQL API (https://developers.linear.app/docs/graphql/working-with-the-graphql-api#queries-and-mutations)
func (p *Linear) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) {
query := []byte(`{"query": "query Me { viewer { id displayName name email avatarUrl active } }"}`)
bodyReader := bytes.NewReader(query)
req, err := http.NewRequestWithContext(p.ctx, "POST", p.userInfoURL, bodyReader)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
return p.sendRawUserInfoRequest(req, token)
}
+3 -3
View File
@@ -42,7 +42,7 @@ func NewMondayProvider() *Monday {
//
// API reference: https://developer.monday.com/api-reference/reference/me
func (p *Monday) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
data, err := p.FetchRawUserData(token)
data, err := p.FetchRawUserInfo(token)
if err != nil {
return nil, err
}
@@ -90,11 +90,11 @@ func (p *Monday) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
return user, nil
}
// FetchRawUserData implements Provider.FetchRawUserData interface.
// FetchRawUserInfo implements Provider.FetchRawUserInfo interface.
//
// monday.com doesn't have a UserInfo endpoint and information on the user
// is retrieved using their GraphQL API (https://developer.monday.com/api-reference/reference/me#queries)
func (p *Monday) FetchRawUserData(token *oauth2.Token) ([]byte, error) {
func (p *Monday) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) {
query := []byte(`{"query": "query { me { id enabled name email is_verified photo_small }}"}`)
bodyReader := bytes.NewReader(query)
+2 -2
View File
@@ -53,7 +53,7 @@ func (p *Strava) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
}
extracted := struct {
Id int `json:"id"`
Id int64 `json:"id"`
FirstName string `json:"firstname"`
LastName string `json:"lastname"`
Username string `json:"username"`
@@ -78,7 +78,7 @@ func (p *Strava) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
user.Expiry, _ = types.ParseDateTime(token.Expiry)
if extracted.Id != 0 {
user.Id = strconv.Itoa(extracted.Id)
user.Id = strconv.FormatInt(extracted.Id, 10)
}
return user, nil
+2 -2
View File
@@ -58,7 +58,7 @@ func (p *VK) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
extracted := struct {
Response []struct {
Id int `json:"id"`
Id int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Username string `json:"screen_name"`
@@ -75,7 +75,7 @@ func (p *VK) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
}
user := &AuthUser{
Id: strconv.Itoa(extracted.Response[0].Id),
Id: strconv.FormatInt(extracted.Response[0].Id, 10),
Name: strings.TrimSpace(extracted.Response[0].FirstName + " " + extracted.Response[0].LastName),
Username: extracted.Response[0].Username,
AvatarURL: extracted.Response[0].AvatarURL,
+8 -6
View File
@@ -112,15 +112,17 @@ func (h *Hook[T]) BindFunc(fn func(e T) error) string {
return h.Bind(&Handler[T]{Func: fn})
}
// Unbind removes a single hook handler by its id.
func (h *Hook[T]) Unbind(id string) {
// Unbind removes one or many hook handler by their id.
func (h *Hook[T]) Unbind(idsToRemove ...string) {
h.mu.Lock()
defer h.mu.Unlock()
for i := len(h.handlers) - 1; i >= 0; i-- {
if h.handlers[i].Id == id {
h.handlers = append(h.handlers[:i], h.handlers[i+1:]...)
break // for now stop on the first occurrence since we don't allow handlers with duplicated ids
for _, id := range idsToRemove {
for i := len(h.handlers) - 1; i >= 0; i-- {
if h.handlers[i].Id == id {
h.handlers = append(h.handlers[:i], h.handlers[i+1:]...)
break // for now stop on the first occurrence since we don't allow handlers with duplicated ids
}
}
}
}
+4 -3
View File
@@ -70,6 +70,7 @@ func TestHookUnbind(t *testing.T) {
calls := ""
id0 := h.BindFunc(func(e *Event) error { calls += "0"; return e.Next() })
id1 := h.BindFunc(func(e *Event) error { calls += "1"; return e.Next() })
h.BindFunc(func(e *Event) error { calls += "2"; return e.Next() })
h.Bind(&Handler[*Event]{
@@ -78,11 +79,11 @@ func TestHookUnbind(t *testing.T) {
h.Unbind("missing") // should do nothing and not panic
if total := len(h.handlers); total != 3 {
t.Fatalf("Expected %d handlers, got %d", 3, total)
if total := len(h.handlers); total != 4 {
t.Fatalf("Expected %d handlers, got %d", 4, total)
}
h.Unbind(id1)
h.Unbind(id1, id0)
if total := len(h.handlers); total != 2 {
t.Fatalf("Expected %d handlers, got %d", 2, total)
+8
View File
@@ -221,6 +221,14 @@ func (e *Event) Stream(status int, contentType string, reader io.Reader) error {
return err
}
// Blob writes a blob (bytes slice) response.
func (e *Event) Blob(status int, contentType string, b []byte) error {
e.setResponseHeaderIfEmpty(headerContentType, contentType)
e.Response.WriteHeader(status)
_, err := e.Response.Write(b)
return err
}
// FileFS serves the specified filename from fsys.
//
// It is similar to [echo.FileFS] for consistency with earlier versions.
+20
View File
@@ -500,6 +500,26 @@ func TestEventStream(t *testing.T) {
}
}
func TestEventBlob(t *testing.T) {
scenarios := []testResponseWriteScenario[[]byte]{
{
name: "blob",
status: 234,
headers: map[string]string{"content-type": "text/test"},
body: []byte("test"),
expectedStatus: 234,
expectedHeaders: map[string]string{"content-type": "text/test"},
expectedBody: "test",
},
}
for _, s := range scenarios {
testEventResponseWrite(t, s, func(e *router.Event) error {
return e.Blob(s.status, s.headers["content-type"], s.body)
})
}
}
func TestEventNoContent(t *testing.T) {
s := testResponseWriteScenario[any]{
name: "no content",
+1 -1
View File
@@ -52,7 +52,7 @@ func (b *Broker) Register(client Client) {
b.store.Set(client.Id(), client)
}
// Unregister removes a single client by its id.
// Unregister removes a single client by its id and marks it as discarded.
//
// If client with clientId doesn't exist, this method does nothing.
func (b *Broker) Unregister(clientId string) {
+10 -2
View File
@@ -32,6 +32,8 @@ type Client interface {
Id() string
// Channel returns the client's communication channel.
//
// NB! The channel shouldn't be used after calling Discard().
Channel() chan Message
// Subscriptions returns a shallow copy of the client subscriptions matching the prefixes.
@@ -65,8 +67,8 @@ type Client interface {
// Get retrieves the key value from the client's context.
Get(key string) any
// Discard marks the client as "discarded", meaning that it
// shouldn't be used anymore for sending new messages.
// Discard marks the client as "discarded" (and closes its channel),
// meaning that it shouldn't be used anymore for sending new messages.
//
// It is safe to call Discard() multiple times.
Discard()
@@ -257,6 +259,12 @@ func (c *DefaultClient) Discard() {
c.mux.Lock()
defer c.mux.Unlock()
if c.isDiscarded {
return
}
close(c.channel)
c.isDiscarded = true
}
+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.23.0-rc13"
PB_VERSION = "v0.23.10"
-39
View File
@@ -1,39 +0,0 @@
import{S as Be,i as Ce,s as Te,U as Le,W as I,f as c,y as w,h as k,c as ae,j as h,l as d,n as a,m as ne,G as Q,X as $e,Y as Se,D as Ue,Z as je,E as De,t as J,a as N,u,d as ie,p as oe,I as Ee,k as O,o as Re,V as qe}from"./index-DGnuRrBU.js";import{F as Fe}from"./FieldsQueryParam-DeLgGM_S.js";function we(n,s,l){const o=n.slice();return o[8]=s[l],o}function Me(n,s,l){const o=n.slice();return o[8]=s[l],o}function Ae(n,s){let l,o=s[8].code+"",p,b,i,f;function m(){return s[6](s[8])}return{key:n,first:null,c(){l=c("button"),p=w(o),b=k(),h(l,"class","tab-item"),O(l,"active",s[1]===s[8].code),this.first=l},m(v,y){d(v,l,y),a(l,p),a(l,b),i||(f=Re(l,"click",m),i=!0)},p(v,y){s=v,y&4&&o!==(o=s[8].code+"")&&Q(p,o),y&6&&O(l,"active",s[1]===s[8].code)},d(v){v&&u(l),i=!1,f()}}}function Pe(n,s){let l,o,p,b;return o=new qe({props:{content:s[8].body}}),{key:n,first:null,c(){l=c("div"),ae(o.$$.fragment),p=k(),h(l,"class","tab-item"),O(l,"active",s[1]===s[8].code),this.first=l},m(i,f){d(i,l,f),ne(o,l,null),a(l,p),b=!0},p(i,f){s=i;const m={};f&4&&(m.content=s[8].body),o.$set(m),(!b||f&6)&&O(l,"active",s[1]===s[8].code)},i(i){b||(J(o.$$.fragment,i),b=!0)},o(i){N(o.$$.fragment,i),b=!1},d(i){i&&u(l),ie(o)}}}function Ge(n){var ke,ge;let s,l,o=n[0].name+"",p,b,i,f,m,v,y,g=n[0].name+"",V,ce,W,M,X,L,Y,A,q,re,F,S,de,Z,G=n[0].name+"",z,ue,K,U,x,P,ee,fe,te,T,le,j,se,B,D,$=[],me=new Map,pe,E,_=[],be=new Map,C;M=new Le({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${n[3]}');
...
const result = await pb.collection('${(ke=n[0])==null?void 0:ke.name}').listAuthMethods();
`,dart:`
import 'package:pocketbase/pocketbase.dart';
final pb = PocketBase('${n[3]}');
...
final result = await pb.collection('${(ge=n[0])==null?void 0:ge.name}').listAuthMethods();
`}}),T=new Fe({});let H=I(n[2]);const he=e=>e[8].code;for(let e=0;e<H.length;e+=1){let t=Me(n,H,e),r=he(t);me.set(r,$[e]=Ae(r,t))}let R=I(n[2]);const _e=e=>e[8].code;for(let e=0;e<R.length;e+=1){let t=we(n,R,e),r=_e(t);be.set(r,_[e]=Pe(r,t))}return{c(){s=c("h3"),l=w("List auth methods ("),p=w(o),b=w(")"),i=k(),f=c("div"),m=c("p"),v=w("Returns a public list with all allowed "),y=c("strong"),V=w(g),ce=w(" authentication methods."),W=k(),ae(M.$$.fragment),X=k(),L=c("h6"),L.textContent="API details",Y=k(),A=c("div"),q=c("strong"),q.textContent="GET",re=k(),F=c("div"),S=c("p"),de=w("/api/collections/"),Z=c("strong"),z=w(G),ue=w("/auth-methods"),K=k(),U=c("div"),U.textContent="Query parameters",x=k(),P=c("table"),ee=c("thead"),ee.innerHTML='<tr><th>Param</th> <th>Type</th> <th width="50%">Description</th></tr>',fe=k(),te=c("tbody"),ae(T.$$.fragment),le=k(),j=c("div"),j.textContent="Responses",se=k(),B=c("div"),D=c("div");for(let e=0;e<$.length;e+=1)$[e].c();pe=k(),E=c("div");for(let e=0;e<_.length;e+=1)_[e].c();h(s,"class","m-b-sm"),h(f,"class","content txt-lg m-b-sm"),h(L,"class","m-b-xs"),h(q,"class","label label-primary"),h(F,"class","content"),h(A,"class","alert alert-info"),h(U,"class","section-title"),h(P,"class","table-compact table-border m-b-base"),h(j,"class","section-title"),h(D,"class","tabs-header compact combined left"),h(E,"class","tabs-content"),h(B,"class","tabs")},m(e,t){d(e,s,t),a(s,l),a(s,p),a(s,b),d(e,i,t),d(e,f,t),a(f,m),a(m,v),a(m,y),a(y,V),a(m,ce),d(e,W,t),ne(M,e,t),d(e,X,t),d(e,L,t),d(e,Y,t),d(e,A,t),a(A,q),a(A,re),a(A,F),a(F,S),a(S,de),a(S,Z),a(Z,z),a(S,ue),d(e,K,t),d(e,U,t),d(e,x,t),d(e,P,t),a(P,ee),a(P,fe),a(P,te),ne(T,te,null),d(e,le,t),d(e,j,t),d(e,se,t),d(e,B,t),a(B,D);for(let r=0;r<$.length;r+=1)$[r]&&$[r].m(D,null);a(B,pe),a(B,E);for(let r=0;r<_.length;r+=1)_[r]&&_[r].m(E,null);C=!0},p(e,[t]){var ve,ye;(!C||t&1)&&o!==(o=e[0].name+"")&&Q(p,o),(!C||t&1)&&g!==(g=e[0].name+"")&&Q(V,g);const r={};t&9&&(r.js=`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${e[3]}');
...
const result = await pb.collection('${(ve=e[0])==null?void 0:ve.name}').listAuthMethods();
`),t&9&&(r.dart=`
import 'package:pocketbase/pocketbase.dart';
final pb = PocketBase('${e[3]}');
...
final result = await pb.collection('${(ye=e[0])==null?void 0:ye.name}').listAuthMethods();
`),M.$set(r),(!C||t&1)&&G!==(G=e[0].name+"")&&Q(z,G),t&6&&(H=I(e[2]),$=$e($,t,he,1,e,H,me,D,Se,Ae,null,Me)),t&6&&(R=I(e[2]),Ue(),_=$e(_,t,_e,1,e,R,be,E,je,Pe,null,we),De())},i(e){if(!C){J(M.$$.fragment,e),J(T.$$.fragment,e);for(let t=0;t<R.length;t+=1)J(_[t]);C=!0}},o(e){N(M.$$.fragment,e),N(T.$$.fragment,e);for(let t=0;t<_.length;t+=1)N(_[t]);C=!1},d(e){e&&(u(s),u(i),u(f),u(W),u(X),u(L),u(Y),u(A),u(K),u(U),u(x),u(P),u(le),u(j),u(se),u(B)),ie(M,e),ie(T);for(let t=0;t<$.length;t+=1)$[t].d();for(let t=0;t<_.length;t+=1)_[t].d()}}}function He(n,s,l){let o,{collection:p}=s,b=200,i=[],f={},m=!1;v();async function v(){l(5,m=!0);try{l(4,f=await oe.collection(p.name).listAuthMethods())}catch(g){oe.error(g)}l(5,m=!1)}const y=g=>l(1,b=g.code);return n.$$set=g=>{"collection"in g&&l(0,p=g.collection)},n.$$.update=()=>{n.$$.dirty&48&&l(2,i=[{code:200,body:m?"...":JSON.stringify(f,null,2)},{code:404,body:`
{
"code": 404,
"message": "Missing collection context.",
"data": {}
}
`}])},l(3,o=Ee.getApiExampleUrl(oe.baseURL)),[p,b,i,o,f,m,y]}class Je extends Be{constructor(s){super(),Ce(this,s,He,Ge,Te,{collection:0})}}export{Je as default};
+39
View File
@@ -0,0 +1,39 @@
import{S as Be,i as Ce,s as Te,V as Le,X as J,h as c,z as w,j as k,c as ae,k as h,n as d,o as a,m as ne,H as Q,Y as $e,Z as Se,E as je,_ as qe,G as Ee,t as z,a as I,v as u,d as ie,p as oe,J as He,l as N,q as Re,W as Ue}from"./index-DBAJ0dmZ.js";import{F as De}from"./FieldsQueryParam-Ci8Vrqy3.js";function we(n,s,l){const o=n.slice();return o[8]=s[l],o}function Me(n,s,l){const o=n.slice();return o[8]=s[l],o}function Ae(n,s){let l,o=s[8].code+"",p,b,i,f;function m(){return s[6](s[8])}return{key:n,first:null,c(){l=c("button"),p=w(o),b=k(),h(l,"class","tab-item"),N(l,"active",s[1]===s[8].code),this.first=l},m(v,y){d(v,l,y),a(l,p),a(l,b),i||(f=Re(l,"click",m),i=!0)},p(v,y){s=v,y&4&&o!==(o=s[8].code+"")&&Q(p,o),y&6&&N(l,"active",s[1]===s[8].code)},d(v){v&&u(l),i=!1,f()}}}function Pe(n,s){let l,o,p,b;return o=new Ue({props:{content:s[8].body}}),{key:n,first:null,c(){l=c("div"),ae(o.$$.fragment),p=k(),h(l,"class","tab-item"),N(l,"active",s[1]===s[8].code),this.first=l},m(i,f){d(i,l,f),ne(o,l,null),a(l,p),b=!0},p(i,f){s=i;const m={};f&4&&(m.content=s[8].body),o.$set(m),(!b||f&6)&&N(l,"active",s[1]===s[8].code)},i(i){b||(z(o.$$.fragment,i),b=!0)},o(i){I(o.$$.fragment,i),b=!1},d(i){i&&u(l),ie(o)}}}function Fe(n){var ke,ge;let s,l,o=n[0].name+"",p,b,i,f,m,v,y,g=n[0].name+"",O,ce,V,M,W,L,X,A,U,re,D,S,de,Y,F=n[0].name+"",Z,ue,K,j,x,P,ee,fe,te,T,le,q,se,B,E,$=[],me=new Map,pe,H,_=[],be=new Map,C;M=new Le({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${n[3]}');
...
const result = await pb.collection('${(ke=n[0])==null?void 0:ke.name}').listAuthMethods();
`,dart:`
import 'package:pocketbase/pocketbase.dart';
final pb = PocketBase('${n[3]}');
...
final result = await pb.collection('${(ge=n[0])==null?void 0:ge.name}').listAuthMethods();
`}}),T=new De({});let G=J(n[2]);const he=e=>e[8].code;for(let e=0;e<G.length;e+=1){let t=Me(n,G,e),r=he(t);me.set(r,$[e]=Ae(r,t))}let R=J(n[2]);const _e=e=>e[8].code;for(let e=0;e<R.length;e+=1){let t=we(n,R,e),r=_e(t);be.set(r,_[e]=Pe(r,t))}return{c(){s=c("h3"),l=w("List auth methods ("),p=w(o),b=w(")"),i=k(),f=c("div"),m=c("p"),v=w("Returns a public list with all allowed "),y=c("strong"),O=w(g),ce=w(" authentication methods."),V=k(),ae(M.$$.fragment),W=k(),L=c("h6"),L.textContent="API details",X=k(),A=c("div"),U=c("strong"),U.textContent="GET",re=k(),D=c("div"),S=c("p"),de=w("/api/collections/"),Y=c("strong"),Z=w(F),ue=w("/auth-methods"),K=k(),j=c("div"),j.textContent="Query parameters",x=k(),P=c("table"),ee=c("thead"),ee.innerHTML='<tr><th>Param</th> <th>Type</th> <th width="50%">Description</th></tr>',fe=k(),te=c("tbody"),ae(T.$$.fragment),le=k(),q=c("div"),q.textContent="Responses",se=k(),B=c("div"),E=c("div");for(let e=0;e<$.length;e+=1)$[e].c();pe=k(),H=c("div");for(let e=0;e<_.length;e+=1)_[e].c();h(s,"class","m-b-sm"),h(f,"class","content txt-lg m-b-sm"),h(L,"class","m-b-xs"),h(U,"class","label label-primary"),h(D,"class","content"),h(A,"class","alert alert-info"),h(j,"class","section-title"),h(P,"class","table-compact table-border m-b-base"),h(q,"class","section-title"),h(E,"class","tabs-header compact combined left"),h(H,"class","tabs-content"),h(B,"class","tabs")},m(e,t){d(e,s,t),a(s,l),a(s,p),a(s,b),d(e,i,t),d(e,f,t),a(f,m),a(m,v),a(m,y),a(y,O),a(m,ce),d(e,V,t),ne(M,e,t),d(e,W,t),d(e,L,t),d(e,X,t),d(e,A,t),a(A,U),a(A,re),a(A,D),a(D,S),a(S,de),a(S,Y),a(Y,Z),a(S,ue),d(e,K,t),d(e,j,t),d(e,x,t),d(e,P,t),a(P,ee),a(P,fe),a(P,te),ne(T,te,null),d(e,le,t),d(e,q,t),d(e,se,t),d(e,B,t),a(B,E);for(let r=0;r<$.length;r+=1)$[r]&&$[r].m(E,null);a(B,pe),a(B,H);for(let r=0;r<_.length;r+=1)_[r]&&_[r].m(H,null);C=!0},p(e,[t]){var ve,ye;(!C||t&1)&&o!==(o=e[0].name+"")&&Q(p,o),(!C||t&1)&&g!==(g=e[0].name+"")&&Q(O,g);const r={};t&9&&(r.js=`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${e[3]}');
...
const result = await pb.collection('${(ve=e[0])==null?void 0:ve.name}').listAuthMethods();
`),t&9&&(r.dart=`
import 'package:pocketbase/pocketbase.dart';
final pb = PocketBase('${e[3]}');
...
final result = await pb.collection('${(ye=e[0])==null?void 0:ye.name}').listAuthMethods();
`),M.$set(r),(!C||t&1)&&F!==(F=e[0].name+"")&&Q(Z,F),t&6&&(G=J(e[2]),$=$e($,t,he,1,e,G,me,E,Se,Ae,null,Me)),t&6&&(R=J(e[2]),je(),_=$e(_,t,_e,1,e,R,be,H,qe,Pe,null,we),Ee())},i(e){if(!C){z(M.$$.fragment,e),z(T.$$.fragment,e);for(let t=0;t<R.length;t+=1)z(_[t]);C=!0}},o(e){I(M.$$.fragment,e),I(T.$$.fragment,e);for(let t=0;t<_.length;t+=1)I(_[t]);C=!1},d(e){e&&(u(s),u(i),u(f),u(V),u(W),u(L),u(X),u(A),u(K),u(j),u(x),u(P),u(le),u(q),u(se),u(B)),ie(M,e),ie(T);for(let t=0;t<$.length;t+=1)$[t].d();for(let t=0;t<_.length;t+=1)_[t].d()}}}function Ge(n,s,l){let o,{collection:p}=s,b=200,i=[],f={},m=!1;v();async function v(){l(5,m=!0);try{l(4,f=await oe.collection(p.name).listAuthMethods())}catch(g){oe.error(g)}l(5,m=!1)}const y=g=>l(1,b=g.code);return n.$$set=g=>{"collection"in g&&l(0,p=g.collection)},n.$$.update=()=>{n.$$.dirty&48&&l(2,i=[{code:200,body:m?"...":JSON.stringify(f,null,2)},{code:404,body:`
{
"code": 404,
"message": "Missing collection context.",
"data": {}
}
`}])},l(3,o=He.getApiExampleUrl(oe.baseURL)),[p,b,i,o,f,m,y]}class ze extends Be{constructor(s){super(),Ce(this,s,Ge,Fe,Te,{collection:0})}}export{ze as default};
-79
View File
@@ -1,79 +0,0 @@
import{S as xe,i as Ie,s as Je,U as Ke,V as Ne,W as J,f as s,y as k,h as p,c as K,j as b,l as d,n as o,m as Q,G as de,X as Le,Y as Qe,D as We,Z as Ge,E as Xe,t as V,a as U,u,d as W,I as Oe,p as Ye,k as G,o as Ze}from"./index-DGnuRrBU.js";import{F as et}from"./FieldsQueryParam-DeLgGM_S.js";function Ve(r,a,l){const n=r.slice();return n[5]=a[l],n}function Ue(r,a,l){const n=r.slice();return n[5]=a[l],n}function je(r,a){let l,n=a[5].code+"",m,_,i,h;function g(){return a[4](a[5])}return{key:r,first:null,c(){l=s("button"),m=k(n),_=p(),b(l,"class","tab-item"),G(l,"active",a[1]===a[5].code),this.first=l},m(v,w){d(v,l,w),o(l,m),o(l,_),i||(h=Ze(l,"click",g),i=!0)},p(v,w){a=v,w&4&&n!==(n=a[5].code+"")&&de(m,n),w&6&&G(l,"active",a[1]===a[5].code)},d(v){v&&u(l),i=!1,h()}}}function ze(r,a){let l,n,m,_;return n=new Ne({props:{content:a[5].body}}),{key:r,first:null,c(){l=s("div"),K(n.$$.fragment),m=p(),b(l,"class","tab-item"),G(l,"active",a[1]===a[5].code),this.first=l},m(i,h){d(i,l,h),Q(n,l,null),o(l,m),_=!0},p(i,h){a=i;const g={};h&4&&(g.content=a[5].body),n.$set(g),(!_||h&6)&&G(l,"active",a[1]===a[5].code)},i(i){_||(V(n.$$.fragment,i),_=!0)},o(i){U(n.$$.fragment,i),_=!1},d(i){i&&u(l),W(n)}}}function tt(r){var qe,Fe;let a,l,n=r[0].name+"",m,_,i,h,g,v,w,D,X,S,j,ue,z,M,pe,Y,N=r[0].name+"",Z,he,fe,x,ee,q,te,T,oe,be,F,C,ae,me,le,_e,f,ke,P,ge,ve,ye,se,$e,ne,Se,we,Te,re,Ce,Re,A,ie,E,ce,R,H,$=[],Pe=new Map,Ae,L,y=[],Be=new Map,B;v=new Ke({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.record.id);
`,dart:`
import 'package:pocketbase/pocketbase.dart';
final pb = PocketBase('${r[3]}');
...
final authData = await pb.collection('${(Fe=r[0])==null?void 0:Fe.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.record.id);
`}}),P=new Ne({props:{content:"?expand=relField1,relField2.subRelField"}}),A=new et({props:{prefix:"record."}});let I=J(r[2]);const De=e=>e[5].code;for(let e=0;e<I.length;e+=1){let t=Ue(r,I,e),c=De(t);Pe.set(c,$[e]=je(c,t))}let O=J(r[2]);const Me=e=>e[5].code;for(let e=0;e<O.length;e+=1){let t=Ve(r,O,e),c=Me(t);Be.set(c,y[e]=ze(c,t))}return{c(){a=s("h3"),l=k("Auth refresh ("),m=k(n),_=k(")"),i=p(),h=s("div"),h.innerHTML=`<p>Returns a new auth response (token and record data) for an
<strong>already authenticated record</strong>.</p> <p>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.</p>`,g=p(),K(v.$$.fragment),w=p(),D=s("h6"),D.textContent="API details",X=p(),S=s("div"),j=s("strong"),j.textContent="POST",ue=p(),z=s("div"),M=s("p"),pe=k("/api/collections/"),Y=s("strong"),Z=k(N),he=k("/auth-refresh"),fe=p(),x=s("p"),x.innerHTML="Requires <code>Authorization:TOKEN</code> header",ee=p(),q=s("div"),q.textContent="Query parameters",te=p(),T=s("table"),oe=s("thead"),oe.innerHTML='<tr><th>Param</th> <th>Type</th> <th width="60%">Description</th></tr>',be=p(),F=s("tbody"),C=s("tr"),ae=s("td"),ae.textContent="expand",me=p(),le=s("td"),le.innerHTML='<span class="label">String</span>',_e=p(),f=s("td"),ke=k(`Auto expand record relations. Ex.:
`),K(P.$$.fragment),ge=k(`
Supports up to 6-levels depth nested relations expansion. `),ve=s("br"),ye=k(`
The expanded relations will be appended to the record under the
`),se=s("code"),se.textContent="expand",$e=k(" property (eg. "),ne=s("code"),ne.textContent='"expand": {"relField1": {...}, ...}',Se=k(`).
`),we=s("br"),Te=k(`
Only the relations to which the request user has permissions to `),re=s("strong"),re.textContent="view",Ce=k(" will be expanded."),Re=p(),K(A.$$.fragment),ie=p(),E=s("div"),E.textContent="Responses",ce=p(),R=s("div"),H=s("div");for(let e=0;e<$.length;e+=1)$[e].c();Ae=p(),L=s("div");for(let e=0;e<y.length;e+=1)y[e].c();b(a,"class","m-b-sm"),b(h,"class","content txt-lg m-b-sm"),b(D,"class","m-b-xs"),b(j,"class","label label-primary"),b(z,"class","content"),b(x,"class","txt-hint txt-sm txt-right"),b(S,"class","alert alert-success"),b(q,"class","section-title"),b(T,"class","table-compact table-border m-b-base"),b(E,"class","section-title"),b(H,"class","tabs-header compact combined left"),b(L,"class","tabs-content"),b(R,"class","tabs")},m(e,t){d(e,a,t),o(a,l),o(a,m),o(a,_),d(e,i,t),d(e,h,t),d(e,g,t),Q(v,e,t),d(e,w,t),d(e,D,t),d(e,X,t),d(e,S,t),o(S,j),o(S,ue),o(S,z),o(z,M),o(M,pe),o(M,Y),o(Y,Z),o(M,he),o(S,fe),o(S,x),d(e,ee,t),d(e,q,t),d(e,te,t),d(e,T,t),o(T,oe),o(T,be),o(T,F),o(F,C),o(C,ae),o(C,me),o(C,le),o(C,_e),o(C,f),o(f,ke),Q(P,f,null),o(f,ge),o(f,ve),o(f,ye),o(f,se),o(f,$e),o(f,ne),o(f,Se),o(f,we),o(f,Te),o(f,re),o(f,Ce),o(F,Re),Q(A,F,null),d(e,ie,t),d(e,E,t),d(e,ce,t),d(e,R,t),o(R,H);for(let c=0;c<$.length;c+=1)$[c]&&$[c].m(H,null);o(R,Ae),o(R,L);for(let c=0;c<y.length;c+=1)y[c]&&y[c].m(L,null);B=!0},p(e,[t]){var Ee,He;(!B||t&1)&&n!==(n=e[0].name+"")&&de(m,n);const c={};t&9&&(c.js=`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${e[3]}');
...
const authData = await pb.collection('${(Ee=e[0])==null?void 0:Ee.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.record.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.record.id);
`),v.$set(c),(!B||t&1)&&N!==(N=e[0].name+"")&&de(Z,N),t&6&&(I=J(e[2]),$=Le($,t,De,1,e,I,Pe,H,Qe,je,null,Ue)),t&6&&(O=J(e[2]),We(),y=Le(y,t,Me,1,e,O,Be,L,Ge,ze,null,Ve),Xe())},i(e){if(!B){V(v.$$.fragment,e),V(P.$$.fragment,e),V(A.$$.fragment,e);for(let t=0;t<O.length;t+=1)V(y[t]);B=!0}},o(e){U(v.$$.fragment,e),U(P.$$.fragment,e),U(A.$$.fragment,e);for(let t=0;t<y.length;t+=1)U(y[t]);B=!1},d(e){e&&(u(a),u(i),u(h),u(g),u(w),u(D),u(X),u(S),u(ee),u(q),u(te),u(T),u(ie),u(E),u(ce),u(R)),W(v,e),W(P),W(A);for(let t=0;t<$.length;t+=1)$[t].d();for(let t=0;t<y.length;t+=1)y[t].d()}}}function ot(r,a,l){let n,{collection:m}=a,_=200,i=[];const h=g=>l(1,_=g.code);return r.$$set=g=>{"collection"in g&&l(0,m=g.collection)},r.$$.update=()=>{r.$$.dirty&1&&l(2,i=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:Oe.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": {}
}
`}])},l(3,n=Oe.getApiExampleUrl(Ye.baseURL)),[m,_,i,n,h]}class st extends xe{constructor(a){super(),Ie(this,a,ot,tt,Je,{collection:0})}}export{st as default};
+79
View File
@@ -0,0 +1,79 @@
import{S as Ue,i as xe,s as Ke,V as Qe,W as Ne,X as K,h as s,z as k,j as p,c as Q,k as b,n as d,o,m as W,H as de,Y as Le,Z as We,E as Ge,_ as Ie,G as Xe,t as V,a as z,v as u,d as G,J as Oe,p as Ye,l as I,q as Ze}from"./index-DBAJ0dmZ.js";import{F as et}from"./FieldsQueryParam-Ci8Vrqy3.js";function Ve(r,a,l){const n=r.slice();return n[5]=a[l],n}function ze(r,a,l){const n=r.slice();return n[5]=a[l],n}function je(r,a){let l,n=a[5].code+"",m,_,i,h;function g(){return a[4](a[5])}return{key:r,first:null,c(){l=s("button"),m=k(n),_=p(),b(l,"class","tab-item"),I(l,"active",a[1]===a[5].code),this.first=l},m(v,w){d(v,l,w),o(l,m),o(l,_),i||(h=Ze(l,"click",g),i=!0)},p(v,w){a=v,w&4&&n!==(n=a[5].code+"")&&de(m,n),w&6&&I(l,"active",a[1]===a[5].code)},d(v){v&&u(l),i=!1,h()}}}function Je(r,a){let l,n,m,_;return n=new Ne({props:{content:a[5].body}}),{key:r,first:null,c(){l=s("div"),Q(n.$$.fragment),m=p(),b(l,"class","tab-item"),I(l,"active",a[1]===a[5].code),this.first=l},m(i,h){d(i,l,h),W(n,l,null),o(l,m),_=!0},p(i,h){a=i;const g={};h&4&&(g.content=a[5].body),n.$set(g),(!_||h&6)&&I(l,"active",a[1]===a[5].code)},i(i){_||(V(n.$$.fragment,i),_=!0)},o(i){z(n.$$.fragment,i),_=!1},d(i){i&&u(l),G(n)}}}function tt(r){var De,Fe;let a,l,n=r[0].name+"",m,_,i,h,g,v,w,B,X,S,j,ue,J,M,pe,Y,N=r[0].name+"",Z,he,fe,U,ee,D,te,T,oe,be,F,C,ae,me,le,_e,f,ke,P,ge,ve,$e,se,ye,ne,Se,we,Te,re,Ce,Re,q,ie,H,ce,R,E,y=[],Pe=new Map,qe,L,$=[],Ae=new Map,A;v=new Qe({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${r[3]}');
...
const 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
console.log(pb.authStore.isValid);
console.log(pb.authStore.token);
console.log(pb.authStore.record.id);
`,dart:`
import 'package:pocketbase/pocketbase.dart';
final pb = PocketBase('${r[3]}');
...
final authData = await pb.collection('${(Fe=r[0])==null?void 0:Fe.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.record.id);
`}}),P=new Ne({props:{content:"?expand=relField1,relField2.subRelField"}}),q=new et({props:{prefix:"record."}});let x=K(r[2]);const Be=e=>e[5].code;for(let e=0;e<x.length;e+=1){let t=ze(r,x,e),c=Be(t);Pe.set(c,y[e]=je(c,t))}let O=K(r[2]);const Me=e=>e[5].code;for(let e=0;e<O.length;e+=1){let t=Ve(r,O,e),c=Me(t);Ae.set(c,$[e]=Je(c,t))}return{c(){a=s("h3"),l=k("Auth refresh ("),m=k(n),_=k(")"),i=p(),h=s("div"),h.innerHTML=`<p>Returns a new auth response (token and record data) for an
<strong>already authenticated record</strong>.</p> <p>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.</p>`,g=p(),Q(v.$$.fragment),w=p(),B=s("h6"),B.textContent="API details",X=p(),S=s("div"),j=s("strong"),j.textContent="POST",ue=p(),J=s("div"),M=s("p"),pe=k("/api/collections/"),Y=s("strong"),Z=k(N),he=k("/auth-refresh"),fe=p(),U=s("p"),U.innerHTML="Requires <code>Authorization:TOKEN</code> header",ee=p(),D=s("div"),D.textContent="Query parameters",te=p(),T=s("table"),oe=s("thead"),oe.innerHTML='<tr><th>Param</th> <th>Type</th> <th width="60%">Description</th></tr>',be=p(),F=s("tbody"),C=s("tr"),ae=s("td"),ae.textContent="expand",me=p(),le=s("td"),le.innerHTML='<span class="label">String</span>',_e=p(),f=s("td"),ke=k(`Auto expand record relations. Ex.:
`),Q(P.$$.fragment),ge=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
`),se=s("code"),se.textContent="expand",ye=k(" property (eg. "),ne=s("code"),ne.textContent='"expand": {"relField1": {...}, ...}',Se=k(`).
`),we=s("br"),Te=k(`
Only the relations to which the request user has permissions to `),re=s("strong"),re.textContent="view",Ce=k(" will be expanded."),Re=p(),Q(q.$$.fragment),ie=p(),H=s("div"),H.textContent="Responses",ce=p(),R=s("div"),E=s("div");for(let e=0;e<y.length;e+=1)y[e].c();qe=p(),L=s("div");for(let e=0;e<$.length;e+=1)$[e].c();b(a,"class","m-b-sm"),b(h,"class","content txt-lg m-b-sm"),b(B,"class","m-b-xs"),b(j,"class","label label-primary"),b(J,"class","content"),b(U,"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(E,"class","tabs-header compact combined left"),b(L,"class","tabs-content"),b(R,"class","tabs")},m(e,t){d(e,a,t),o(a,l),o(a,m),o(a,_),d(e,i,t),d(e,h,t),d(e,g,t),W(v,e,t),d(e,w,t),d(e,B,t),d(e,X,t),d(e,S,t),o(S,j),o(S,ue),o(S,J),o(J,M),o(M,pe),o(M,Y),o(Y,Z),o(M,he),o(S,fe),o(S,U),d(e,ee,t),d(e,D,t),d(e,te,t),d(e,T,t),o(T,oe),o(T,be),o(T,F),o(F,C),o(C,ae),o(C,me),o(C,le),o(C,_e),o(C,f),o(f,ke),W(P,f,null),o(f,ge),o(f,ve),o(f,$e),o(f,se),o(f,ye),o(f,ne),o(f,Se),o(f,we),o(f,Te),o(f,re),o(f,Ce),o(F,Re),W(q,F,null),d(e,ie,t),d(e,H,t),d(e,ce,t),d(e,R,t),o(R,E);for(let c=0;c<y.length;c+=1)y[c]&&y[c].m(E,null);o(R,qe),o(R,L);for(let c=0;c<$.length;c+=1)$[c]&&$[c].m(L,null);A=!0},p(e,[t]){var He,Ee;(!A||t&1)&&n!==(n=e[0].name+"")&&de(m,n);const c={};t&9&&(c.js=`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${e[3]}');
...
const 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
console.log(pb.authStore.isValid);
console.log(pb.authStore.token);
console.log(pb.authStore.record.id);
`),t&9&&(c.dart=`
import 'package:pocketbase/pocketbase.dart';
final pb = PocketBase('${e[3]}');
...
final authData = await pb.collection('${(Ee=e[0])==null?void 0:Ee.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.record.id);
`),v.$set(c),(!A||t&1)&&N!==(N=e[0].name+"")&&de(Z,N),t&6&&(x=K(e[2]),y=Le(y,t,Be,1,e,x,Pe,E,We,je,null,ze)),t&6&&(O=K(e[2]),Ge(),$=Le($,t,Me,1,e,O,Ae,L,Ie,Je,null,Ve),Xe())},i(e){if(!A){V(v.$$.fragment,e),V(P.$$.fragment,e),V(q.$$.fragment,e);for(let t=0;t<O.length;t+=1)V($[t]);A=!0}},o(e){z(v.$$.fragment,e),z(P.$$.fragment,e),z(q.$$.fragment,e);for(let t=0;t<$.length;t+=1)z($[t]);A=!1},d(e){e&&(u(a),u(i),u(h),u(g),u(w),u(B),u(X),u(S),u(ee),u(D),u(te),u(T),u(ie),u(H),u(ce),u(R)),G(v,e),G(P),G(q);for(let t=0;t<y.length;t+=1)y[t].d();for(let t=0;t<$.length;t+=1)$[t].d()}}}function ot(r,a,l){let n,{collection:m}=a,_=200,i=[];const h=g=>l(1,_=g.code);return r.$$set=g=>{"collection"in g&&l(0,m=g.collection)},r.$$.update=()=>{r.$$.dirty&1&&l(2,i=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:Oe.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": {}
}
`}])},l(3,n=Oe.getApiExampleUrl(Ye.baseURL)),[m,_,i,n,h]}class st extends Ue{constructor(a){super(),xe(this,a,ot,tt,Ke,{collection:0})}}export{st as default};
@@ -1,4 +1,4 @@
import{S as Ee,i as Je,s as xe,U as Ie,V as Ve,W as Q,f as o,y as k,h,c as z,j as p,l as r,n as a,m as G,G as pe,X as Ue,Y as Ne,D as Qe,Z as ze,E as Ge,t as V,a as E,u as c,d as K,I as Be,p as Ke,k as X,o as Xe}from"./index-DGnuRrBU.js";import{F as Ye}from"./FieldsQueryParam-DeLgGM_S.js";function Fe(s,l,n){const i=s.slice();return i[5]=l[n],i}function Le(s,l,n){const i=s.slice();return i[5]=l[n],i}function je(s,l){let n,i=l[5].code+"",f,g,d,b;function _(){return l[4](l[5])}return{key:s,first:null,c(){n=o("button"),f=k(i),g=h(),p(n,"class","tab-item"),X(n,"active",l[1]===l[5].code),this.first=n},m(v,O){r(v,n,O),a(n,f),a(n,g),d||(b=Xe(n,"click",_),d=!0)},p(v,O){l=v,O&4&&i!==(i=l[5].code+"")&&pe(f,i),O&6&&X(n,"active",l[1]===l[5].code)},d(v){v&&c(n),d=!1,b()}}}function He(s,l){let n,i,f,g;return i=new Ve({props:{content:l[5].body}}),{key:s,first:null,c(){n=o("div"),z(i.$$.fragment),f=h(),p(n,"class","tab-item"),X(n,"active",l[1]===l[5].code),this.first=n},m(d,b){r(d,n,b),G(i,n,null),a(n,f),g=!0},p(d,b){l=d;const _={};b&4&&(_.content=l[5].body),i.$set(_),(!g||b&6)&&X(n,"active",l[1]===l[5].code)},i(d){g||(V(i.$$.fragment,d),g=!0)},o(d){E(i.$$.fragment,d),g=!1},d(d){d&&c(n),K(i)}}}function Ze(s){let l,n,i=s[0].name+"",f,g,d,b,_,v,O,R,Y,y,J,be,x,P,me,Z,I=s[0].name+"",ee,fe,te,M,ae,W,le,U,ne,A,oe,ge,B,S,se,ke,ie,_e,m,ve,C,we,$e,Oe,re,ye,ce,Ae,Se,Te,de,Ce,qe,q,ue,F,he,T,L,$=[],De=new Map,Re,j,w=[],Pe=new Map,D;v=new Ie({props:{js:`
import{S as Ee,i as Je,s as xe,V as ze,W as Ve,X as Q,h as o,z as _,j as h,c as G,k as p,n as r,o as a,m as I,H as pe,Y as Ue,Z as Ne,E as Qe,_ as Ge,G as Ie,t as V,a as E,v as c,d as K,J as Be,p as Ke,l as X,q as Xe}from"./index-DBAJ0dmZ.js";import{F as Ye}from"./FieldsQueryParam-Ci8Vrqy3.js";function Fe(s,l,n){const i=s.slice();return i[5]=l[n],i}function He(s,l,n){const i=s.slice();return i[5]=l[n],i}function Le(s,l){let n,i=l[5].code+"",f,g,d,b;function k(){return l[4](l[5])}return{key:s,first:null,c(){n=o("button"),f=_(i),g=h(),p(n,"class","tab-item"),X(n,"active",l[1]===l[5].code),this.first=n},m(v,O){r(v,n,O),a(n,f),a(n,g),d||(b=Xe(n,"click",k),d=!0)},p(v,O){l=v,O&4&&i!==(i=l[5].code+"")&&pe(f,i),O&6&&X(n,"active",l[1]===l[5].code)},d(v){v&&c(n),d=!1,b()}}}function je(s,l){let n,i,f,g;return i=new Ve({props:{content:l[5].body}}),{key:s,first:null,c(){n=o("div"),G(i.$$.fragment),f=h(),p(n,"class","tab-item"),X(n,"active",l[1]===l[5].code),this.first=n},m(d,b){r(d,n,b),I(i,n,null),a(n,f),g=!0},p(d,b){l=d;const k={};b&4&&(k.content=l[5].body),i.$set(k),(!g||b&6)&&X(n,"active",l[1]===l[5].code)},i(d){g||(V(i.$$.fragment,d),g=!0)},o(d){E(i.$$.fragment,d),g=!1},d(d){d&&c(n),K(i)}}}function Ze(s){let l,n,i=s[0].name+"",f,g,d,b,k,v,O,D,Y,A,J,be,x,P,me,Z,z=s[0].name+"",ee,fe,te,M,ae,W,le,U,ne,y,oe,ge,B,S,se,_e,ie,ke,m,ve,C,we,$e,Oe,re,Ae,ce,ye,Se,Te,de,Ce,qe,q,ue,F,he,T,H,$=[],Re=new Map,De,L,w=[],Pe=new Map,R;v=new ze({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${s[3]}');
@@ -45,18 +45,18 @@ import{S as Ee,i as Je,s as xe,U as Ie,V as Ve,W as Q,f as o,y as k,h,c as z,j a
// "logout"
pb.authStore.clear();
`}}),C=new Ve({props:{content:"?expand=relField1,relField2.subRelField"}}),q=new Ye({props:{prefix:"record."}});let N=Q(s[2]);const Me=e=>e[5].code;for(let e=0;e<N.length;e+=1){let t=Le(s,N,e),u=Me(t);De.set(u,$[e]=je(u,t))}let H=Q(s[2]);const We=e=>e[5].code;for(let e=0;e<H.length;e+=1){let t=Fe(s,H,e),u=We(t);Pe.set(u,w[e]=He(u,t))}return{c(){l=o("h3"),n=k("Auth with OAuth2 ("),f=k(i),g=k(")"),d=h(),b=o("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
`}}),C=new Ve({props:{content:"?expand=relField1,relField2.subRelField"}}),q=new Ye({props:{prefix:"record."}});let N=Q(s[2]);const Me=e=>e[5].code;for(let e=0;e<N.length;e+=1){let t=He(s,N,e),u=Me(t);Re.set(u,$[e]=Le(u,t))}let j=Q(s[2]);const We=e=>e[5].code;for(let e=0;e<j.length;e+=1){let t=Fe(s,j,e),u=We(t);Pe.set(u,w[e]=je(u,t))}return{c(){l=o("h3"),n=_("Auth with OAuth2 ("),f=_(i),g=_(")"),d=h(),b=o("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/#authenticate-with-oauth2" target="_blank" rel="noopener noreferrer">OAuth2 integration documentation
</a>.</p>`,_=h(),z(v.$$.fragment),O=h(),R=o("h6"),R.textContent="API details",Y=h(),y=o("div"),J=o("strong"),J.textContent="POST",be=h(),x=o("div"),P=o("p"),me=k("/api/collections/"),Z=o("strong"),ee=k(I),fe=k("/auth-with-oauth2"),te=h(),M=o("div"),M.textContent="Body Parameters",ae=h(),W=o("table"),W.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>provider</span></div></td> <td><span class="label">String</span></td> <td>The name of the OAuth2 client provider (eg. &quot;google&quot;).</td></tr> <tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>code</span></div></td> <td><span class="label">String</span></td> <td>The authorization code returned from the initial request.</td></tr> <tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>codeVerifier</span></div></td> <td><span class="label">String</span></td> <td>The code verifier sent with the initial request as part of the code_challenge.</td></tr> <tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>redirectURL</span></div></td> <td><span class="label">String</span></td> <td>The redirect url sent with the initial request.</td></tr> <tr><td><div class="inline-flex"><span class="label label-warning">Optional</span> <span>createData</span></div></td> <td><span class="label">Object</span></td> <td><p>Optional data that will be used when creating the auth record on OAuth2 sign-up.</p> <p>The created auth record must comply with the same requirements and validations in the
</a>.</p>`,k=h(),G(v.$$.fragment),O=h(),D=o("h6"),D.textContent="API details",Y=h(),A=o("div"),J=o("strong"),J.textContent="POST",be=h(),x=o("div"),P=o("p"),me=_("/api/collections/"),Z=o("strong"),ee=_(z),fe=_("/auth-with-oauth2"),te=h(),M=o("div"),M.textContent="Body Parameters",ae=h(),W=o("table"),W.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>provider</span></div></td> <td><span class="label">String</span></td> <td>The name of the OAuth2 client provider (eg. &quot;google&quot;).</td></tr> <tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>code</span></div></td> <td><span class="label">String</span></td> <td>The authorization code returned from the initial request.</td></tr> <tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>codeVerifier</span></div></td> <td><span class="label">String</span></td> <td>The code verifier sent with the initial request as part of the code_challenge.</td></tr> <tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>redirectURL</span></div></td> <td><span class="label">String</span></td> <td>The redirect url sent with the initial request.</td></tr> <tr><td><div class="inline-flex"><span class="label label-warning">Optional</span> <span>createData</span></div></td> <td><span class="label">Object</span></td> <td><p>Optional data that will be used when creating the auth record on OAuth2 sign-up.</p> <p>The created auth record must comply with the same requirements and validations in the
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>`,le=h(),U=o("div"),U.textContent="Query parameters",ne=h(),A=o("table"),oe=o("thead"),oe.innerHTML='<tr><th>Param</th> <th>Type</th> <th width="60%">Description</th></tr>',ge=h(),B=o("tbody"),S=o("tr"),se=o("td"),se.textContent="expand",ke=h(),ie=o("td"),ie.innerHTML='<span class="label">String</span>',_e=h(),m=o("td"),ve=k(`Auto expand record relations. Ex.:
`),z(C.$$.fragment),we=k(`
Supports up to 6-levels depth nested relations expansion. `),$e=o("br"),Oe=k(`
upload currently are not supported during OAuth2 sign-ups.</em></p></td></tr></tbody>`,le=h(),U=o("div"),U.textContent="Query parameters",ne=h(),y=o("table"),oe=o("thead"),oe.innerHTML='<tr><th>Param</th> <th>Type</th> <th width="60%">Description</th></tr>',ge=h(),B=o("tbody"),S=o("tr"),se=o("td"),se.textContent="expand",_e=h(),ie=o("td"),ie.innerHTML='<span class="label">String</span>',ke=h(),m=o("td"),ve=_(`Auto expand record relations. Ex.:
`),G(C.$$.fragment),we=_(`
Supports up to 6-levels depth nested relations expansion. `),$e=o("br"),Oe=_(`
The expanded relations will be appended to the record under the
`),re=o("code"),re.textContent="expand",ye=k(" property (eg. "),ce=o("code"),ce.textContent='"expand": {"relField1": {...}, ...}',Ae=k(`).
`),Se=o("br"),Te=k(`
Only the relations to which the request user has permissions to `),de=o("strong"),de.textContent="view",Ce=k(" will be expanded."),qe=h(),z(q.$$.fragment),ue=h(),F=o("div"),F.textContent="Responses",he=h(),T=o("div"),L=o("div");for(let e=0;e<$.length;e+=1)$[e].c();Re=h(),j=o("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(R,"class","m-b-xs"),p(J,"class","label label-primary"),p(x,"class","content"),p(y,"class","alert alert-success"),p(M,"class","section-title"),p(W,"class","table-compact table-border m-b-base"),p(U,"class","section-title"),p(A,"class","table-compact table-border m-b-base"),p(F,"class","section-title"),p(L,"class","tabs-header compact combined left"),p(j,"class","tabs-content"),p(T,"class","tabs")},m(e,t){r(e,l,t),a(l,n),a(l,f),a(l,g),r(e,d,t),r(e,b,t),r(e,_,t),G(v,e,t),r(e,O,t),r(e,R,t),r(e,Y,t),r(e,y,t),a(y,J),a(y,be),a(y,x),a(x,P),a(P,me),a(P,Z),a(Z,ee),a(P,fe),r(e,te,t),r(e,M,t),r(e,ae,t),r(e,W,t),r(e,le,t),r(e,U,t),r(e,ne,t),r(e,A,t),a(A,oe),a(A,ge),a(A,B),a(B,S),a(S,se),a(S,ke),a(S,ie),a(S,_e),a(S,m),a(m,ve),G(C,m,null),a(m,we),a(m,$e),a(m,Oe),a(m,re),a(m,ye),a(m,ce),a(m,Ae),a(m,Se),a(m,Te),a(m,de),a(m,Ce),a(B,qe),G(q,B,null),r(e,ue,t),r(e,F,t),r(e,he,t),r(e,T,t),a(T,L);for(let u=0;u<$.length;u+=1)$[u]&&$[u].m(L,null);a(T,Re),a(T,j);for(let u=0;u<w.length;u+=1)w[u]&&w[u].m(j,null);D=!0},p(e,[t]){(!D||t&1)&&i!==(i=e[0].name+"")&&pe(f,i);const u={};t&9&&(u.js=`
`),re=o("code"),re.textContent="expand",Ae=_(" property (eg. "),ce=o("code"),ce.textContent='"expand": {"relField1": {...}, ...}',ye=_(`).
`),Se=o("br"),Te=_(`
Only the relations to which the request user has permissions to `),de=o("strong"),de.textContent="view",Ce=_(" will be expanded."),qe=h(),G(q.$$.fragment),ue=h(),F=o("div"),F.textContent="Responses",he=h(),T=o("div"),H=o("div");for(let e=0;e<$.length;e+=1)$[e].c();De=h(),L=o("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(x,"class","content"),p(A,"class","alert alert-success"),p(M,"class","section-title"),p(W,"class","table-compact table-border m-b-base"),p(U,"class","section-title"),p(y,"class","table-compact table-border m-b-base"),p(F,"class","section-title"),p(H,"class","tabs-header compact combined left"),p(L,"class","tabs-content"),p(T,"class","tabs")},m(e,t){r(e,l,t),a(l,n),a(l,f),a(l,g),r(e,d,t),r(e,b,t),r(e,k,t),I(v,e,t),r(e,O,t),r(e,D,t),r(e,Y,t),r(e,A,t),a(A,J),a(A,be),a(A,x),a(x,P),a(P,me),a(P,Z),a(Z,ee),a(P,fe),r(e,te,t),r(e,M,t),r(e,ae,t),r(e,W,t),r(e,le,t),r(e,U,t),r(e,ne,t),r(e,y,t),a(y,oe),a(y,ge),a(y,B),a(B,S),a(S,se),a(S,_e),a(S,ie),a(S,ke),a(S,m),a(m,ve),I(C,m,null),a(m,we),a(m,$e),a(m,Oe),a(m,re),a(m,Ae),a(m,ce),a(m,ye),a(m,Se),a(m,Te),a(m,de),a(m,Ce),a(B,qe),I(q,B,null),r(e,ue,t),r(e,F,t),r(e,he,t),r(e,T,t),a(T,H);for(let u=0;u<$.length;u+=1)$[u]&&$[u].m(H,null);a(T,De),a(T,L);for(let u=0;u<w.length;u+=1)w[u]&&w[u].m(L,null);R=!0},p(e,[t]){(!R||t&1)&&i!==(i=e[0].name+"")&&pe(f,i);const u={};t&9&&(u.js=`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${e[3]}');
@@ -103,7 +103,7 @@ import{S as Ee,i as Je,s as xe,U as Ie,V as Ve,W as Q,f as o,y as k,h,c as z,j a
// "logout"
pb.authStore.clear();
`),v.$set(u),(!D||t&1)&&I!==(I=e[0].name+"")&&pe(ee,I),t&6&&(N=Q(e[2]),$=Ue($,t,Me,1,e,N,De,L,Ne,je,null,Le)),t&6&&(H=Q(e[2]),Qe(),w=Ue(w,t,We,1,e,H,Pe,j,ze,He,null,Fe),Ge())},i(e){if(!D){V(v.$$.fragment,e),V(C.$$.fragment,e),V(q.$$.fragment,e);for(let t=0;t<H.length;t+=1)V(w[t]);D=!0}},o(e){E(v.$$.fragment,e),E(C.$$.fragment,e),E(q.$$.fragment,e);for(let t=0;t<w.length;t+=1)E(w[t]);D=!1},d(e){e&&(c(l),c(d),c(b),c(_),c(O),c(R),c(Y),c(y),c(te),c(M),c(ae),c(W),c(le),c(U),c(ne),c(A),c(ue),c(F),c(he),c(T)),K(v,e),K(C),K(q);for(let t=0;t<$.length;t+=1)$[t].d();for(let t=0;t<w.length;t+=1)w[t].d()}}}function et(s,l,n){let i,{collection:f}=l,g=200,d=[];const b=_=>n(1,g=_.code);return s.$$set=_=>{"collection"in _&&n(0,f=_.collection)},s.$$.update=()=>{s.$$.dirty&1&&n(2,d=[{code:200,body:JSON.stringify({token:"JWT_AUTH_TOKEN",record:Be.dummyCollectionRecord(f),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(u),(!R||t&1)&&z!==(z=e[0].name+"")&&pe(ee,z),t&6&&(N=Q(e[2]),$=Ue($,t,Me,1,e,N,Re,H,Ne,Le,null,He)),t&6&&(j=Q(e[2]),Qe(),w=Ue(w,t,We,1,e,j,Pe,L,Ge,je,null,Fe),Ie())},i(e){if(!R){V(v.$$.fragment,e),V(C.$$.fragment,e),V(q.$$.fragment,e);for(let t=0;t<j.length;t+=1)V(w[t]);R=!0}},o(e){E(v.$$.fragment,e),E(C.$$.fragment,e),E(q.$$.fragment,e);for(let t=0;t<w.length;t+=1)E(w[t]);R=!1},d(e){e&&(c(l),c(d),c(b),c(k),c(O),c(D),c(Y),c(A),c(te),c(M),c(ae),c(W),c(le),c(U),c(ne),c(y),c(ue),c(F),c(he),c(T)),K(v,e),K(C),K(q);for(let t=0;t<$.length;t+=1)$[t].d();for(let t=0;t<w.length;t+=1)w[t].d()}}}function et(s,l,n){let i,{collection:f}=l,g=200,d=[];const b=k=>n(1,g=k.code);return s.$$set=k=>{"collection"in k&&n(0,f=k.collection)},s.$$.update=()=>{s.$$.dirty&1&&n(2,d=[{code:200,body:JSON.stringify({token:"JWT_AUTH_TOKEN",record:Be.dummyCollectionRecord(f),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.",

Some files were not shown because too many files have changed in this diff Show More