mirror of
https://github.com/pocketbase/pocketbase.git
synced 2026-09-30 11:31:46 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1f1d19d7f | ||
|
|
5200f9c493 | ||
|
|
2c8aa2e5fa | ||
|
|
eeae1f64ee | ||
|
|
506172c495 | ||
|
|
d75f5f663c | ||
|
|
92e15c287e | ||
|
|
dd895dee01 | ||
|
|
4b2e75992b | ||
|
|
c498c918ec | ||
|
|
125e99e4c8 | ||
|
|
5461120b04 | ||
|
|
4a7fb95650 | ||
|
|
444fa78252 | ||
|
|
641aa54cfc | ||
|
|
f015911594 | ||
|
|
fadb2e68a2 | ||
|
|
5ca79eb85d | ||
|
|
62c8523070 | ||
|
|
8debafa755 | ||
|
|
0089ceb904 | ||
|
|
6443f2f159 | ||
|
|
0e12169546 | ||
|
|
9d7856a9eb | ||
|
|
306045fa2f | ||
|
|
a9c42d0282 | ||
|
|
f318f461ea | ||
|
|
51bc9f3982 | ||
|
|
2c6f99418f | ||
|
|
3f3b77dcd4 | ||
|
|
db679f9620 | ||
|
|
c76ee987bd | ||
|
|
0d4da9c3be | ||
|
|
fab56f688d | ||
|
|
1610729d92 | ||
|
|
6522bc55b1 | ||
|
|
c8776b7cd9 | ||
|
|
262e78c04e | ||
|
|
0a66e5a286 | ||
|
|
cdfaed4fa3 | ||
|
|
0f73679c3f | ||
|
|
e09f71ae74 | ||
|
|
17082de560 | ||
|
|
88bb8c406e | ||
|
|
2ab50cc77e | ||
|
|
0025ae80ad | ||
|
|
568c63b29f | ||
|
|
6e9a9489a7 | ||
|
|
e73077e7e7 | ||
|
|
0113fecca9 | ||
|
|
fbc378067d | ||
|
|
e80d64414b | ||
|
|
7ffe9f63a5 | ||
|
|
5dbd9821e8 | ||
|
|
87c6c5b483 | ||
|
|
836fc77ddc | ||
|
|
3ef752c232 | ||
|
|
c7c590bace | ||
|
|
fac0d5b899 | ||
|
|
902e07e724 | ||
|
|
dc350f0a3e | ||
|
|
1a3efe96ac | ||
|
|
18f152a0e7 | ||
|
|
52dcb4192c | ||
|
|
08b1b49e17 |
No files matched your search
@@ -28,7 +28,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '>=1.23.6'
|
||||
go-version: '>=1.23.12'
|
||||
|
||||
# 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.
|
||||
|
||||
+84
-1
@@ -1,3 +1,86 @@
|
||||
## v0.29.2
|
||||
|
||||
- Bumped min Go GitHub action version to 1.23.12 since it comes with some [minor fixes for the runtime and `database/sql` package](https://github.com/golang/go/issues?q=milestone%3AGo1.23.12+label%3ACherryPickApproved).
|
||||
|
||||
|
||||
## v0.29.1
|
||||
|
||||
- Updated the X/Twitter provider to return the `confirmed_email` field and to use the `x.com` domain ([#7035](https://github.com/pocketbase/pocketbase/issues/7035)).
|
||||
|
||||
- Added Box.com OAuth2 provider ([#7056](https://github.com/pocketbase/pocketbase/pull/7056); thanks @blakepatteson).
|
||||
|
||||
- Updated `modernc.org/sqlite` to 1.38.2 (SQLite 3.50.3).
|
||||
|
||||
- Fixed example List API response ([#7049](https://github.com/pocketbase/pocketbase/pull/7049); thanks @williamtguerra).
|
||||
|
||||
|
||||
## v0.29.0
|
||||
|
||||
- Enabled calling the `/auth-refresh` endpoint with nonrenewable tokens.
|
||||
_When used with nonrenewable tokens (e.g. impersonate) the endpoint will simply return the same token with the up-to-date user data associated with it._
|
||||
|
||||
- Added the triggered rate rimit rule in the error log `details`.
|
||||
|
||||
- Added optional `ServeEvent.Listener` field to initialize a custom network listener (e.g. `unix`) instead of the default `tcp` ([#3233](https://github.com/pocketbase/pocketbase/discussions/3233)).
|
||||
|
||||
- Fixed request data unmarshalization for the `DynamicModel` array/object fields ([#7022](https://github.com/pocketbase/pocketbase/discussions/7022)).
|
||||
|
||||
- Fixed Dashboard page title `-` escaping ([#6982](https://github.com/pocketbase/pocketbase/issues/6982)).
|
||||
|
||||
- Other minor improvements (updated first superuser console text when running with `go run`, clarified trusted IP proxy header label, wrapped the backup restore in a transaction as an extra precaution, updated deps, etc.).
|
||||
|
||||
|
||||
## v0.28.4
|
||||
|
||||
- Added global JSVM `toBytes()` helper to return the bytes slice representation of a value such as io.Reader or string, _other types are first serialized to Go string_ ([#6935](https://github.com/pocketbase/pocketbase/issues/6935)).
|
||||
|
||||
- Fixed `security.RandomStringByRegex` random distribution ([#6947](https://github.com/pocketbase/pocketbase/pull/6947); thanks @yerTools).
|
||||
|
||||
- Minor docs and typos fixes.
|
||||
|
||||
|
||||
## v0.28.3
|
||||
|
||||
- Skip sending empty `Range` header when fetching blobs from S3 ([#6914](https://github.com/pocketbase/pocketbase/pull/6914)).
|
||||
|
||||
- Updated Go deps and particularly `modernc.org/sqlite` to 1.38.0 (SQLite 3.50.1).
|
||||
|
||||
- Bumped GitHub action min Go version to 1.23.10 as it comes with some [minor security `net/http` fixes](https://github.com/golang/go/issues?q=milestone%3AGo1.23.10+label%3ACherryPickApproved).
|
||||
|
||||
|
||||
## v0.28.2
|
||||
|
||||
- Loaded latin-ext charset for the default text fonts ([#6869](https://github.com/pocketbase/pocketbase/issues/6869)).
|
||||
|
||||
- Updated view query CAST regex to properly recognize multiline expressions ([#6860](https://github.com/pocketbase/pocketbase/pull/6860); thanks @azat-ismagilov).
|
||||
|
||||
- Updated Go and npm dependencies.
|
||||
|
||||
|
||||
## v0.28.1
|
||||
|
||||
- Fixed `json_each`/`json_array_length` normalizations to properly check for array values ([#6835](https://github.com/pocketbase/pocketbase/issues/6835)).
|
||||
|
||||
|
||||
## v0.28.0
|
||||
|
||||
- Write the default response body of `*Request` hooks that are wrapped in a transaction after the related transaction completes to allow propagating the transaction error ([#6462](https://github.com/pocketbase/pocketbase/discussions/6462#discussioncomment-12207818)).
|
||||
|
||||
- Updated `app.DB()` to automatically routes raw write SQL statements to the nonconcurrent db pool ([#6689](https://github.com/pocketbase/pocketbase/discussions/6689)).
|
||||
_For the rare cases when it is needed users still have the option to explicitly target the specific pool they want using `app.ConcurrentDB()`/`app.NonconcurrentDB()`._
|
||||
|
||||
- ⚠️ Changed the default `json` field max size to 1MB.
|
||||
_Users still have the option to adjust the default limit from the collection field options but keep in mind that storing large strings/blobs in the database is known to cause performance issues and should be avoided when possible._
|
||||
|
||||
- ⚠️ Soft-deprecated and replaced `filesystem.System.GetFile(fileKey)` with `filesystem.System.GetReader(fileKey)` to avoid the confusion with `filesystem.File`.
|
||||
_The old method will still continue to work for at least until v0.29.0 but you'll get a console warning to replace it with `GetReader`._
|
||||
|
||||
- Added new `filesystem.System.GetReuploadableFile(fileKey, preserveName)` method to return an existing blob as a `*filesystem.File` value ([#6792](https://github.com/pocketbase/pocketbase/discussions/6792)).
|
||||
_This method could be useful in case you want to clone an existing Record file and assign it to a new Record (e.g. in a Record duplicate action)._
|
||||
|
||||
- Other minor improvements (updated the GitHub release min Go version to 1.23.9, updated npm and Go deps, etc.)
|
||||
|
||||
|
||||
## v0.27.2
|
||||
|
||||
- Added workers pool when cascade deleting record files to minimize _"thread exhaustion"_ errors ([#6780](https://github.com/pocketbase/pocketbase/discussions/6780)).
|
||||
@@ -444,7 +527,7 @@ There are a lot of changes but to highlight some of the most notable ones:
|
||||
- Admins are now system `_superusers` auth records.
|
||||
- Builtin rate limiter (_supports tags, wildcards and exact routes matching_).
|
||||
- Batch/transactional Web API endpoint.
|
||||
- Impersonate Web API endpoint (_it could be also used for generating fixed/non-refreshable superuser tokens, aka. "API keys"_).
|
||||
- Impersonate Web API endpoint (_it could be also used for generating fixed/nonrenewable superuser tokens, aka. "API keys"_).
|
||||
- Support for custom user request activity log attributes.
|
||||
- One-Time Password (OTP) auth method (_via email code_).
|
||||
- Multi-Factor Authentication (MFA) support (_currently requires any 2 different auth methods to be used_).
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
> For the most recent versions, please refer to [CHANGELOG.md](./CHANGELOG.md)
|
||||
---
|
||||
|
||||
## v0.22.35
|
||||
|
||||
- (_Backported from v0.29.2_) Bumped min Go GitHub action version to 1.23.12 since it comes with some [minor fixes for the runtime and `database/sql` package](https://github.com/golang/go/issues?q=milestone%3AGo1.23.12+label%3ACherryPickApproved).
|
||||
|
||||
|
||||
## v0.22.34
|
||||
|
||||
- (_Backported from v0.26.6_) Allow OIDC `email_verified` to be int or boolean string since some OIDC providers like AWS Cognito has non-standard userinfo response ([#6657](https://github.com/pocketbase/pocketbase/pull/6657)).
|
||||
|
||||
+5
-5
@@ -49,7 +49,7 @@ var ValidBatchActions = map[*regexp.Regexp]BatchActionHandlerFunc{
|
||||
params["id"] = id // required for the path value
|
||||
ir.Method = "PATCH"
|
||||
ir.URL = "/api/collections/" + params["collection"] + "/records/" + id + params["query"]
|
||||
return recordUpdate(next)
|
||||
return recordUpdate(false, next)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,16 +57,16 @@ var ValidBatchActions = map[*regexp.Regexp]BatchActionHandlerFunc{
|
||||
// ---
|
||||
ir.Method = "POST"
|
||||
ir.URL = "/api/collections/" + params["collection"] + "/records" + params["query"]
|
||||
return recordCreate(next)
|
||||
return recordCreate(false, next)
|
||||
},
|
||||
regexp.MustCompile(`^POST /api/collections/(?P<collection>[^\/\?]+)/records(\?.*)?$`): func(app core.App, ir *core.InternalRequest, params map[string]string, next func(any) error) HandleFunc {
|
||||
return recordCreate(next)
|
||||
return recordCreate(false, next)
|
||||
},
|
||||
regexp.MustCompile(`^PATCH /api/collections/(?P<collection>[^\/\?]+)/records/(?P<id>[^\/\?]+)(\?.*)?$`): func(app core.App, ir *core.InternalRequest, params map[string]string, next func(any) error) HandleFunc {
|
||||
return recordUpdate(next)
|
||||
return recordUpdate(false, next)
|
||||
},
|
||||
regexp.MustCompile(`^DELETE /api/collections/(?P<collection>[^\/\?]+)/records/(?P<id>[^\/\?]+)(\?.*)?$`): func(app core.App, ir *core.InternalRequest, params map[string]string, next func(any) error) HandleFunc {
|
||||
return recordDelete(next)
|
||||
return recordDelete(false, next)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+15
-5
@@ -45,7 +45,9 @@ func collectionsList(e *core.RequestEvent) error {
|
||||
event.Result = result
|
||||
|
||||
return event.App.OnCollectionsListRequest().Trigger(event, func(e *core.CollectionsListRequestEvent) error {
|
||||
return e.JSON(http.StatusOK, e.Result)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.JSON(http.StatusOK, e.Result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,7 +62,9 @@ func collectionView(e *core.RequestEvent) error {
|
||||
event.Collection = collection
|
||||
|
||||
return e.App.OnCollectionViewRequest().Trigger(event, func(e *core.CollectionRequestEvent) error {
|
||||
return e.JSON(http.StatusOK, e.Collection)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.JSON(http.StatusOK, e.Collection)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -98,7 +102,9 @@ func collectionCreate(e *core.RequestEvent) error {
|
||||
return e.BadRequestError("Failed to create collection. Raw error: \n"+err.Error(), nil)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, e.Collection)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.JSON(http.StatusOK, e.Collection)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -128,7 +134,9 @@ func collectionUpdate(e *core.RequestEvent) error {
|
||||
return e.BadRequestError("Failed to update collection. Raw error: \n"+err.Error(), nil)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, e.Collection)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.JSON(http.StatusOK, e.Collection)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -159,7 +167,9 @@ func collectionDelete(e *core.RequestEvent) error {
|
||||
return e.BadRequestError(msg, err)
|
||||
}
|
||||
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@ func collectionsImport(e *core.RequestEvent) error {
|
||||
return event.App.OnCollectionsImportRequest().Trigger(event, func(e *core.CollectionsImportRequestEvent) error {
|
||||
importErr := e.App.ImportCollections(e.CollectionsData, form.DeleteMissing)
|
||||
if importErr == nil {
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
})
|
||||
}
|
||||
|
||||
// validation failure
|
||||
|
||||
@@ -316,6 +316,51 @@ func TestCollectionsImport(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnCollectionsImportRequest tx body write check",
|
||||
Method: http.MethodPut,
|
||||
URL: "/api/collections/import",
|
||||
Body: strings.NewReader(`{
|
||||
"deleteMissing": true,
|
||||
"collections":[
|
||||
{"name": "test123"},
|
||||
{
|
||||
"id":"wsmn24bux7wo113",
|
||||
"name":"demo1",
|
||||
"fields":[
|
||||
{
|
||||
"id":"_2hlxbmp",
|
||||
"name":"title",
|
||||
"type":"text",
|
||||
"required":true
|
||||
}
|
||||
],
|
||||
"indexes": []
|
||||
}
|
||||
]
|
||||
}`),
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnCollectionsImportRequest().BindFunc(func(e *core.CollectionsImportRequestEvent) error {
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedEvents: map[string]int{"OnCollectionsImportRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
|
||||
+94
-22
@@ -1,7 +1,6 @@
|
||||
package apis_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -130,6 +129,32 @@ func TestCollectionsList(t *testing.T) {
|
||||
"OnCollectionsListRequest": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnCollectionsListRequest tx body write check",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/collections",
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnCollectionsListRequest().BindFunc(func(e *core.CollectionsListRequestEvent) error {
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedEvents: map[string]int{"OnCollectionsListRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
@@ -205,6 +230,32 @@ func TestCollectionView(t *testing.T) {
|
||||
"OnCollectionViewRequest": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnCollectionViewRequest tx body write check",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/collections/wsmn24bux7wo113",
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnCollectionViewRequest().BindFunc(func(e *core.CollectionRequestEvent) error {
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedEvents: map[string]int{"OnCollectionViewRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
@@ -361,7 +412,7 @@ func TestCollectionDelete(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnCollectionAfterDeleteSuccessRequest error response",
|
||||
Name: "OnCollectionDeleteRequest tx body write check",
|
||||
Method: http.MethodDelete,
|
||||
URL: "/api/collections/view2",
|
||||
Headers: map[string]string{
|
||||
@@ -369,15 +420,22 @@ func TestCollectionDelete(t *testing.T) {
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnCollectionDeleteRequest().BindFunc(func(e *core.CollectionRequestEvent) error {
|
||||
return errors.New("error")
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnCollectionDeleteRequest": 1,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnCollectionDeleteRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -656,7 +714,7 @@ func TestCollectionCreate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnCollectionCreateRequest error response",
|
||||
Name: "OnCollectionCreateRequest tx body write check",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections",
|
||||
Body: strings.NewReader(`{"name":"new","type":"base"}`),
|
||||
@@ -665,15 +723,22 @@ func TestCollectionCreate(t *testing.T) {
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnCollectionCreateRequest().BindFunc(func(e *core.CollectionRequestEvent) error {
|
||||
return errors.New("error")
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnCollectionCreateRequest": 1,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnCollectionCreateRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
|
||||
// view
|
||||
@@ -978,7 +1043,7 @@ func TestCollectionUpdate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnCollectionAfterUpdateSuccessRequest error response",
|
||||
Name: "OnCollectionUpdateRequest tx body write check",
|
||||
Method: http.MethodPatch,
|
||||
URL: "/api/collections/demo1",
|
||||
Body: strings.NewReader(`{}`),
|
||||
@@ -987,15 +1052,22 @@ func TestCollectionUpdate(t *testing.T) {
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnCollectionUpdateRequest().BindFunc(func(e *core.CollectionRequestEvent) error {
|
||||
return errors.New("error")
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnCollectionUpdateRequest": 1,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnCollectionUpdateRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
{
|
||||
Name: "authorized as superuser + invalid data (eg. existing name)",
|
||||
|
||||
+6
-3
@@ -75,8 +75,8 @@ func (api *fileApi) fileToken(e *core.RequestEvent) error {
|
||||
event.Record = e.Auth
|
||||
|
||||
return e.App.OnFileTokenRequest().Trigger(event, func(e *core.FileTokenRequestEvent) error {
|
||||
return e.JSON(http.StatusOK, map[string]string{
|
||||
"token": e.Token,
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.JSON(http.StatusOK, map[string]string{"token": e.Token})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -192,7 +192,10 @@ func (api *fileApi) download(e *core.RequestEvent) error {
|
||||
e.Response.Header().Del("X-Frame-Options")
|
||||
|
||||
return e.App.OnFileDownloadRequest().Trigger(event, func(e *core.FileDownloadRequestEvent) error {
|
||||
if err := fsys.Serve(e.Response, e.Request, e.ServedPath, e.ServedName); err != nil {
|
||||
err = execAfterSuccessTx(true, e.App, func() error {
|
||||
return fsys.Serve(e.Response, e.Request, e.ServedPath, e.ServedName)
|
||||
})
|
||||
if err != nil {
|
||||
return e.NotFoundError("", err)
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -32,7 +32,7 @@ func DefaultInstallerFunc(app core.App, systemSuperuser *core.Record, baseURL st
|
||||
_ = osutils.LaunchURL(url)
|
||||
color.Magenta("\n(!) Launch the URL below in the browser if it hasn't been open already to create your first superuser account:")
|
||||
color.New(color.Bold).Add(color.FgCyan).Println(url)
|
||||
color.New(color.FgHiBlack, color.Italic).Printf("(you can also create your first superuser by running: %s superuser upsert EMAIL PASS)\n\n", os.Args[0])
|
||||
color.New(color.FgHiBlack, color.Italic).Printf("(you can also create your first superuser by running: %s superuser upsert EMAIL PASS)\n\n", executablePath())
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -86,3 +86,12 @@ func findOrCreateInstallerSuperuser(app core.App) (*core.Record, error) {
|
||||
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func executablePath() string {
|
||||
// most likely ran with go run
|
||||
if strings.HasPrefix(os.Args[0], os.TempDir()) {
|
||||
return "go run ."
|
||||
}
|
||||
|
||||
return os.Args[0]
|
||||
}
|
||||
+17
-3
@@ -365,11 +365,25 @@ func logRequest(event *core.RequestEvent, err error) {
|
||||
|
||||
// parse the request error
|
||||
if err != nil {
|
||||
if apiErr, ok := err.(*router.ApiError); ok {
|
||||
status = apiErr.Status
|
||||
apiErr, isPlainApiError := err.(*router.ApiError)
|
||||
if isPlainApiError || errors.As(err, &apiErr) {
|
||||
// the status header wasn't written yet
|
||||
if status == 0 {
|
||||
status = apiErr.Status
|
||||
}
|
||||
|
||||
var errMsg string
|
||||
if isPlainApiError {
|
||||
errMsg = apiErr.Message
|
||||
} else {
|
||||
// wrapped ApiError -> add the full serialized version
|
||||
// of the original error since it could contain more information
|
||||
errMsg = err.Error()
|
||||
}
|
||||
|
||||
attrs = append(
|
||||
attrs,
|
||||
slog.String("error", apiErr.Message),
|
||||
slog.String("error", errMsg),
|
||||
slog.Any("details", apiErr.RawData()),
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -167,7 +168,7 @@ func checkRateLimit(e *core.RequestEvent, rtId string, rule core.RateLimitRule)
|
||||
}
|
||||
|
||||
if !rt.isAllowed(key) {
|
||||
return e.TooManyRequestsError("", nil)
|
||||
return e.TooManyRequestsError("", errors.New("triggered rate limit rule: "+rule.String()))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+4
-2
@@ -213,7 +213,9 @@ func realtimeSetSubscriptions(e *core.RequestEvent) error {
|
||||
slog.Any("subscriptions", e.Subscriptions),
|
||||
)
|
||||
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -756,7 +758,7 @@ func realtimeCanAccessRecord(
|
||||
|
||||
var exists int
|
||||
|
||||
q := app.DB().Select("(1)").
|
||||
q := app.ConcurrentDB().Select("(1)").
|
||||
From(record.Collection().Name).
|
||||
AndWhere(dbx.HashExp{record.Collection().Name + ".id": record.Id})
|
||||
|
||||
|
||||
@@ -45,7 +45,9 @@ func recordConfirmEmailChange(e *core.RequestEvent) error {
|
||||
return firstApiError(err, e.BadRequestError("Failed to confirm email change.", err))
|
||||
}
|
||||
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package apis_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -136,7 +135,7 @@ func TestRecordConfirmEmailChange(t *testing.T) {
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "OnRecordAfterConfirmEmailChangeRequest error response",
|
||||
Name: "OnRecordConfirmEmailChangeRequest tx body write check",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/users/confirm-email-change",
|
||||
Body: strings.NewReader(`{
|
||||
@@ -145,15 +144,22 @@ func TestRecordConfirmEmailChange(t *testing.T) {
|
||||
}`),
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnRecordConfirmEmailChangeRequest().BindFunc(func(e *core.RecordConfirmEmailChangeRequestEvent) error {
|
||||
return errors.New("error")
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnRecordConfirmEmailChangeRequest": 1,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnRecordConfirmEmailChangeRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
|
||||
// rate limit checks
|
||||
|
||||
@@ -43,7 +43,9 @@ func recordRequestEmailChange(e *core.RequestEvent) error {
|
||||
return firstApiError(err, e.BadRequestError("Failed to request email change.", err))
|
||||
}
|
||||
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,33 @@ func TestRecordRequestEmailChange(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnRecordRequestEmailChangeRequest tx body write check",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/users/request-email-change",
|
||||
Body: strings.NewReader(`{"newEmail":"change@example.com"}`),
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo",
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnRecordRequestEmailChangeRequest().BindFunc(func(e *core.RecordRequestEmailChangeRequestEvent) error {
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedEvents: map[string]int{"OnRecordRequestEmailChangeRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
|
||||
// rate limit checks
|
||||
// -----------------------------------------------------------
|
||||
|
||||
@@ -26,10 +26,10 @@ func recordAuthImpersonate(e *core.RequestEvent) error {
|
||||
|
||||
form := &impersonateForm{}
|
||||
if err = e.BindBody(form); err != nil {
|
||||
return firstApiError(err, e.BadRequestError("An error occurred while loading the submitted data.", err))
|
||||
return e.BadRequestError("An error occurred while loading the submitted data.", err)
|
||||
}
|
||||
if err = form.validate(); err != nil {
|
||||
return firstApiError(err, e.BadRequestError("An error occurred while validating the submitted data.", err))
|
||||
return e.BadRequestError("An error occurred while validating the submitted data.", err)
|
||||
}
|
||||
|
||||
token, err := record.NewStaticAuthToken(time.Duration(form.Duration) * time.Second)
|
||||
|
||||
@@ -108,8 +108,8 @@ func recordRequestOTP(e *core.RequestEvent) error {
|
||||
})
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, map[string]string{
|
||||
"otpId": otp.Id,
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.JSON(http.StatusOK, map[string]string{"otpId": otp.Id})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -247,6 +247,31 @@ func TestRecordRequestOTP(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnRecordRequestOTPRequest tx body write check",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/users/request-otp",
|
||||
Body: strings.NewReader(`{"email":"test@example.com"}`),
|
||||
Delay: 100 * time.Millisecond,
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnRecordRequestOTPRequest().BindFunc(func(e *core.RecordCreateOTPRequestEvent) error {
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedEvents: map[string]int{"OnRecordRequestOTPRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
|
||||
// rate limit checks
|
||||
// -----------------------------------------------------------
|
||||
|
||||
@@ -54,7 +54,9 @@ func recordConfirmPasswordReset(e *core.RequestEvent) error {
|
||||
|
||||
e.App.Store().Remove(getPasswordResetResendKey(authRecord))
|
||||
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package apis_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -282,7 +281,7 @@ func TestRecordConfirmPasswordReset(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnRecordAfterConfirmPasswordResetRequest error response",
|
||||
Name: "OnRecordConfirmPasswordResetRequest tx body write check",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/users/confirm-password-reset",
|
||||
Body: strings.NewReader(`{
|
||||
@@ -292,15 +291,22 @@ func TestRecordConfirmPasswordReset(t *testing.T) {
|
||||
}`),
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnRecordConfirmPasswordResetRequest().BindFunc(func(e *core.RecordConfirmPasswordResetRequestEvent) error {
|
||||
return errors.New("error")
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnRecordConfirmPasswordResetRequest": 1,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnRecordConfirmPasswordResetRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
|
||||
// rate limit checks
|
||||
|
||||
@@ -65,7 +65,9 @@ func recordRequestPasswordReset(e *core.RequestEvent) error {
|
||||
})
|
||||
})
|
||||
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,30 @@ func TestRecordRequestPasswordReset(t *testing.T) {
|
||||
app.Store().Set(resendKey, struct{}{})
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnRecordRequestPasswordResetRequest tx body write check",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/users/request-password-reset",
|
||||
Body: strings.NewReader(`{"email":"test@example.com"}`),
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnRecordRequestPasswordResetRequest().BindFunc(func(e *core.RecordRequestPasswordResetRequestEvent) error {
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedEvents: map[string]int{"OnRecordRequestPasswordResetRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
|
||||
// rate limit checks
|
||||
// -----------------------------------------------------------
|
||||
|
||||
@@ -12,18 +12,24 @@ func recordAuthRefresh(e *core.RequestEvent) error {
|
||||
return e.NotFoundError("Missing auth record context.", nil)
|
||||
}
|
||||
|
||||
currentToken := getAuthTokenFromRequest(e)
|
||||
claims, _ := security.ParseUnverifiedJWT(currentToken)
|
||||
if v, ok := claims[core.TokenClaimRefreshable]; !ok || !cast.ToBool(v) {
|
||||
return e.ForbiddenError("The current auth token is not refreshable.", nil)
|
||||
}
|
||||
|
||||
event := new(core.RecordAuthRefreshRequestEvent)
|
||||
event.RequestEvent = e
|
||||
event.Collection = record.Collection()
|
||||
event.Record = record
|
||||
|
||||
return e.App.OnRecordAuthRefreshRequest().Trigger(event, func(e *core.RecordAuthRefreshRequestEvent) error {
|
||||
return RecordAuthResponse(e.RequestEvent, e.Record, "", nil)
|
||||
token := getAuthTokenFromRequest(e.RequestEvent)
|
||||
|
||||
// skip token renewal if the token's payload doesn't explicitly allow it (e.g. impersonate tokens)
|
||||
claims, _ := security.ParseUnverifiedJWT(token) //
|
||||
if v, ok := claims[core.TokenClaimRefreshable]; ok && cast.ToBool(v) {
|
||||
var tokenErr error
|
||||
token, tokenErr = e.Record.NewAuthToken()
|
||||
if tokenErr != nil {
|
||||
return e.InternalServerError("Failed to refresh auth token.", tokenErr)
|
||||
}
|
||||
}
|
||||
|
||||
return recordAuthResponse(e.RequestEvent, e.Record, token, "", nil)
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package apis_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
@@ -74,6 +73,8 @@ func TestRecordAuthRefresh(t *testing.T) {
|
||||
},
|
||||
NotExpectedContent: []string{
|
||||
`"missing":`,
|
||||
// should return a different token
|
||||
"eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo",
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
@@ -89,9 +90,21 @@ func TestRecordAuthRefresh(t *testing.T) {
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6ZmFsc2V9.4IsO6YMsR19crhwl_YWzvRH8pfq2Ri4Gv2dzGyneLak",
|
||||
},
|
||||
ExpectedStatus: 403,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
// should return the same token
|
||||
`"token":"eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6ZmFsc2V9.4IsO6YMsR19crhwl_YWzvRH8pfq2Ri4Gv2dzGyneLak"`,
|
||||
`"record":`,
|
||||
`"id":"4q1xlclmfloku33"`,
|
||||
`"emailVisibility":false`,
|
||||
`"email":"test@example.com"`, // the owner can always view their email address
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnRecordAuthRefreshRequest": 1,
|
||||
"OnRecordAuthRequest": 1,
|
||||
"OnRecordEnrich": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "unverified auth record in onlyVerified collection",
|
||||
@@ -130,23 +143,30 @@ func TestRecordAuthRefresh(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnRecordAfterAuthRefreshRequest error response",
|
||||
Name: "OnRecordAuthRefreshRequest tx body write check",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/users/auth-refresh?expand=rel,missing",
|
||||
URL: "/api/collections/users/auth-refresh",
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo",
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnRecordAuthRefreshRequest().BindFunc(func(e *core.RecordAuthRefreshRequestEvent) error {
|
||||
return errors.New("error")
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnRecordAuthRefreshRequest": 1,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnRecordAuthRefreshRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
|
||||
// rate limit checks
|
||||
|
||||
@@ -42,19 +42,19 @@ func recordConfirmVerification(e *core.RequestEvent) error {
|
||||
event.Record = record
|
||||
|
||||
return e.App.OnRecordConfirmVerificationRequest().Trigger(event, func(e *core.RecordConfirmVerificationRequestEvent) error {
|
||||
if wasVerified {
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
}
|
||||
if !wasVerified {
|
||||
e.Record.SetVerified(true)
|
||||
|
||||
e.Record.SetVerified(true)
|
||||
|
||||
if err := e.App.Save(e.Record); err != nil {
|
||||
return firstApiError(err, e.BadRequestError("An error occurred while saving the verified state.", err))
|
||||
if err := e.App.Save(e.Record); err != nil {
|
||||
return firstApiError(err, e.BadRequestError("An error occurred while saving the verified state.", err))
|
||||
}
|
||||
}
|
||||
|
||||
e.App.Store().Remove(getVerificationResendKey(e.Record))
|
||||
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package apis_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -144,7 +143,7 @@ func TestRecordConfirmVerification(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnRecordAfterConfirmVerificationRequest error response",
|
||||
Name: "OnRecordConfirmVerificationRequest tx body write check",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/users/confirm-verification",
|
||||
Body: strings.NewReader(`{
|
||||
@@ -152,15 +151,22 @@ func TestRecordConfirmVerification(t *testing.T) {
|
||||
}`),
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnRecordConfirmVerificationRequest().BindFunc(func(e *core.RecordConfirmVerificationRequestEvent) error {
|
||||
return errors.New("error")
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnRecordConfirmVerificationRequest": 1,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnRecordConfirmVerificationRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
|
||||
// rate limit checks
|
||||
|
||||
@@ -68,7 +68,9 @@ func recordRequestVerification(e *core.RequestEvent) error {
|
||||
})
|
||||
})
|
||||
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,30 @@ func TestRecordRequestVerification(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnRecordRequestVerificationRequest tx body write check",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/users/request-verification",
|
||||
Body: strings.NewReader(`{"email":"test@example.com"}`),
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnRecordRequestVerificationRequest().BindFunc(func(e *core.RecordRequestVerificationRequestEvent) error {
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedEvents: map[string]int{"OnRecordRequestVerificationRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
|
||||
// rate limit checks
|
||||
// -----------------------------------------------------------
|
||||
|
||||
@@ -1577,6 +1577,69 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
|
||||
"OnRecordValidate": 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnRecordAuthWithOAuth2Request tx body write check",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/users/auth-with-oauth2",
|
||||
Body: strings.NewReader(`{
|
||||
"provider": "test",
|
||||
"code":"123",
|
||||
"redirectURL": "https://example.com"
|
||||
}`),
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// register the test provider
|
||||
auth.Providers["test"] = func() auth.Provider {
|
||||
return &oauth2MockProvider{
|
||||
AuthUser: &auth.AuthUser{Id: "test_id"},
|
||||
Token: &oauth2.Token{AccessToken: "abc"},
|
||||
}
|
||||
}
|
||||
|
||||
// add the test provider in the collection
|
||||
user.Collection().MFA.Enabled = false
|
||||
user.Collection().OAuth2.Enabled = true
|
||||
user.Collection().OAuth2.Providers = []core.OAuth2ProviderConfig{{
|
||||
Name: "test",
|
||||
ClientId: "123",
|
||||
ClientSecret: "456",
|
||||
}}
|
||||
if err := app.Save(user.Collection()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// stub linked provider
|
||||
ea := core.NewExternalAuth(app)
|
||||
ea.SetCollectionRef(user.Collection().Id)
|
||||
ea.SetRecordRef(user.Id)
|
||||
ea.SetProvider("test")
|
||||
ea.SetProviderId("test_id")
|
||||
if err := app.Save(ea); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
app.OnRecordAuthWithOAuth2Request().BindFunc(func(e *core.RecordAuthWithOAuth2RequestEvent) error {
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedEvents: map[string]int{"OnRecordAuthWithOAuth2Request": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
|
||||
// rate limit checks
|
||||
// -----------------------------------------------------------
|
||||
|
||||
@@ -419,6 +419,53 @@ func TestRecordAuthWithOTP(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnRecordAuthWithOTPRequest tx body write check",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/users/auth-with-otp",
|
||||
Body: strings.NewReader(`{
|
||||
"otpId":"` + strings.Repeat("a", 15) + `",
|
||||
"password":"123456"
|
||||
}`),
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// disable MFA
|
||||
user.Collection().MFA.Enabled = false
|
||||
if err = app.Save(user.Collection()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
otp := core.NewOTP(app)
|
||||
otp.Id = strings.Repeat("a", 15)
|
||||
otp.SetCollectionRef(user.Collection().Id)
|
||||
otp.SetRecordRef(user.Id)
|
||||
otp.SetPassword("123456")
|
||||
if err := app.Save(otp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
app.OnRecordAuthWithOTPRequest().BindFunc(func(e *core.RecordAuthWithOTPRequestEvent) error {
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedEvents: map[string]int{"OnRecordAuthWithOTPRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
|
||||
// rate limit checks
|
||||
// -----------------------------------------------------------
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package apis_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -82,7 +81,7 @@ func TestRecordAuthWithPassword(t *testing.T) {
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "OnRecordAuthWithPasswordRequest error response",
|
||||
Name: "OnRecordAuthWithPasswordRequest tx body write check",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/clients/auth-with-password",
|
||||
Body: strings.NewReader(`{
|
||||
@@ -91,15 +90,22 @@ func TestRecordAuthWithPassword(t *testing.T) {
|
||||
}`),
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnRecordAuthWithPasswordRequest().BindFunc(func(e *core.RecordAuthWithPasswordRequestEvent) error {
|
||||
return errors.New("error")
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnRecordAuthWithPasswordRequest": 1,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnRecordAuthWithPasswordRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
{
|
||||
Name: "valid identity field and invalid password",
|
||||
|
||||
+24
-14
@@ -28,9 +28,9 @@ func bindRecordCrudApi(app core.App, rg *router.RouterGroup[*core.RequestEvent])
|
||||
subGroup := rg.Group("/collections/{collection}/records").Unbind(DefaultRateLimitMiddlewareId)
|
||||
subGroup.GET("", recordsList)
|
||||
subGroup.GET("/{id}", recordView)
|
||||
subGroup.POST("", recordCreate(nil)).Bind(dynamicCollectionBodyLimit(""))
|
||||
subGroup.PATCH("/{id}", recordUpdate(nil)).Bind(dynamicCollectionBodyLimit(""))
|
||||
subGroup.DELETE("/{id}", recordDelete(nil))
|
||||
subGroup.POST("", recordCreate(true, nil)).Bind(dynamicCollectionBodyLimit(""))
|
||||
subGroup.PATCH("/{id}", recordUpdate(true, nil)).Bind(dynamicCollectionBodyLimit(""))
|
||||
subGroup.DELETE("/{id}", recordDelete(true, nil))
|
||||
}
|
||||
|
||||
func recordsList(e *core.RequestEvent) error {
|
||||
@@ -121,7 +121,9 @@ func recordsList(e *core.RequestEvent) error {
|
||||
randomizedThrottle(150)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, e.Result)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.JSON(http.StatusOK, e.Result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -192,11 +194,13 @@ func recordView(e *core.RequestEvent) error {
|
||||
return firstApiError(err, e.InternalServerError("Failed to enrich record", err))
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, e.Record)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.JSON(http.StatusOK, e.Record)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func recordCreate(optFinalizer func(data any) error) func(e *core.RequestEvent) error {
|
||||
func recordCreate(responseWriteAfterTx bool, optFinalizer func(data any) error) func(e *core.RequestEvent) error {
|
||||
return func(e *core.RequestEvent) error {
|
||||
collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue("collection"))
|
||||
if err != nil || collection == nil {
|
||||
@@ -296,7 +300,7 @@ func recordCreate(optFinalizer func(data any) error) func(e *core.RequestEvent)
|
||||
|
||||
// check non-empty create rule
|
||||
if *dummyCollection.CreateRule != "" {
|
||||
ruleQuery := e.App.DB().Select("(1)").PreFragment(withFrom).From(dummyCollection.Name).AndBind(dummyParams)
|
||||
ruleQuery := e.App.ConcurrentDB().Select("(1)").PreFragment(withFrom).From(dummyCollection.Name).AndBind(dummyParams)
|
||||
|
||||
resolver := core.NewRecordFieldResolver(e.App, &dummyCollection, requestInfo, true)
|
||||
|
||||
@@ -316,7 +320,7 @@ func recordCreate(optFinalizer func(data any) error) func(e *core.RequestEvent)
|
||||
}
|
||||
|
||||
// check for manage rule access
|
||||
manageRuleQuery := e.App.DB().Select("(1)").PreFragment(withFrom).From(dummyCollection.Name).AndBind(dummyParams)
|
||||
manageRuleQuery := e.App.ConcurrentDB().Select("(1)").PreFragment(withFrom).From(dummyCollection.Name).AndBind(dummyParams)
|
||||
if !form.HasManageAccess() &&
|
||||
hasAuthManageAccess(e.App, requestInfo, &dummyCollection, manageRuleQuery) {
|
||||
form.GrantManagerAccess()
|
||||
@@ -344,7 +348,9 @@ func recordCreate(optFinalizer func(data any) error) func(e *core.RequestEvent)
|
||||
return firstApiError(err, e.InternalServerError("Failed to enrich record", err))
|
||||
}
|
||||
|
||||
err = e.JSON(http.StatusOK, e.Record)
|
||||
err = execAfterSuccessTx(responseWriteAfterTx, e.App, func() error {
|
||||
return e.JSON(http.StatusOK, e.Record)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -374,7 +380,7 @@ func recordCreate(optFinalizer func(data any) error) func(e *core.RequestEvent)
|
||||
}
|
||||
}
|
||||
|
||||
func recordUpdate(optFinalizer func(data any) error) func(e *core.RequestEvent) error {
|
||||
func recordUpdate(responseWriteAfterTx bool, optFinalizer func(data any) error) func(e *core.RequestEvent) error {
|
||||
return func(e *core.RequestEvent) error {
|
||||
collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue("collection"))
|
||||
if err != nil || collection == nil {
|
||||
@@ -446,7 +452,7 @@ func recordUpdate(optFinalizer func(data any) error) func(e *core.RequestEvent)
|
||||
}
|
||||
form.Load(data)
|
||||
|
||||
manageRuleQuery := e.App.DB().Select("(1)").From(collection.Name).AndWhere(dbx.HashExp{
|
||||
manageRuleQuery := e.App.ConcurrentDB().Select("(1)").From(collection.Name).AndWhere(dbx.HashExp{
|
||||
collection.Name + ".id": record.Id,
|
||||
})
|
||||
if !form.HasManageAccess() &&
|
||||
@@ -475,7 +481,9 @@ func recordUpdate(optFinalizer func(data any) error) func(e *core.RequestEvent)
|
||||
return firstApiError(err, e.InternalServerError("Failed to enrich record", err))
|
||||
}
|
||||
|
||||
err = e.JSON(http.StatusOK, e.Record)
|
||||
err = execAfterSuccessTx(responseWriteAfterTx, e.App, func() error {
|
||||
return e.JSON(http.StatusOK, e.Record)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -505,7 +513,7 @@ func recordUpdate(optFinalizer func(data any) error) func(e *core.RequestEvent)
|
||||
}
|
||||
}
|
||||
|
||||
func recordDelete(optFinalizer func(data any) error) func(e *core.RequestEvent) error {
|
||||
func recordDelete(responseWriteAfterTx bool, optFinalizer func(data any) error) func(e *core.RequestEvent) error {
|
||||
return func(e *core.RequestEvent) error {
|
||||
collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue("collection"))
|
||||
if err != nil || collection == nil {
|
||||
@@ -565,7 +573,9 @@ func recordDelete(optFinalizer func(data any) error) func(e *core.RequestEvent)
|
||||
return firstApiError(err, e.BadRequestError("Failed to delete record. Make sure that the record is not part of a required relation reference.", err))
|
||||
}
|
||||
|
||||
err = e.NoContent(http.StatusNoContent)
|
||||
err = execAfterSuccessTx(responseWriteAfterTx, e.App, func() error {
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+100
-22
@@ -2,7 +2,6 @@ package apis_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -418,6 +417,32 @@ func TestRecordCrudList(t *testing.T) {
|
||||
"OnRecordsListRequest": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnRecordsListRequest tx body write check",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/collections/demo4/records",
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnRecordsListRequest().BindFunc(func(e *core.RecordsListRequestEvent) error {
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
|
||||
// auth collection
|
||||
// -----------------------------------------------------------
|
||||
@@ -862,6 +887,32 @@ func TestRecordCrudView(t *testing.T) {
|
||||
"OnRecordEnrich": 7,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnRecordViewRequest tx body write check",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/collections/demo1/records/al1h9ijdeojtsjy",
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnRecordViewRequest().BindFunc(func(e *core.RecordRequestEvent) error {
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
|
||||
// auth collection
|
||||
// -----------------------------------------------------------
|
||||
@@ -1209,7 +1260,7 @@ func TestRecordCrudDelete(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnRecordAfterDeleteSuccessRequest error response",
|
||||
Name: "OnRecordDeleteRequest tx body write check",
|
||||
Method: http.MethodDelete,
|
||||
URL: "/api/collections/clients/records/o1y0dd0spd786md",
|
||||
Headers: map[string]string{
|
||||
@@ -1217,15 +1268,22 @@ func TestRecordCrudDelete(t *testing.T) {
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnRecordDeleteRequest().BindFunc(func(e *core.RecordRequestEvent) error {
|
||||
return errors.New("error")
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnRecordDeleteRequest": 1,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnRecordDeleteRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
{
|
||||
Name: "authenticated record that match the collection delete rule",
|
||||
@@ -1792,21 +1850,31 @@ func TestRecordCrudCreate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnRecordAfterCreateSuccessRequest error response",
|
||||
Name: "OnRecordCreateRequest tx body write check",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/demo2/records",
|
||||
Body: strings.NewReader(`{"title":"new"}`),
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnRecordCreateRequest().BindFunc(func(e *core.RecordRequestEvent) error {
|
||||
return errors.New("error")
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnRecordCreateRequest": 1,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnRecordCreateRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
|
||||
// ID checks
|
||||
@@ -2799,21 +2867,31 @@ func TestRecordCrudUpdate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnRecordAfterUpdateSuccessRequest error response",
|
||||
Name: "OnRecordUpdateRequest tx body write check",
|
||||
Method: http.MethodPatch,
|
||||
URL: "/api/collections/demo2/records/0yxhwia2amd8gec",
|
||||
Body: strings.NewReader(`{"title":"new"}`),
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnRecordUpdateRequest().BindFunc(func(e *core.RecordRequestEvent) error {
|
||||
return errors.New("error")
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnRecordUpdateRequest": 1,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnRecordUpdateRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
{
|
||||
Name: "try to change the id of an existing record",
|
||||
|
||||
+26
-3
@@ -129,7 +129,9 @@ func recordAuthResponse(e *core.RequestEvent, authRecord *core.Record, token str
|
||||
result.Meta = e.Meta
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, result)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.JSON(http.StatusOK, result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -455,8 +457,8 @@ func autoResolveRecordsFlags(app core.App, records []*core.Record, requestInfo *
|
||||
managedIds := []string{}
|
||||
|
||||
query := app.RecordQuery(collection).
|
||||
Select(app.DB().QuoteSimpleColumnName(collection.Name) + ".id").
|
||||
AndWhere(dbx.In(app.DB().QuoteSimpleColumnName(collection.Name)+".id", recordIds...))
|
||||
Select(app.ConcurrentDB().QuoteSimpleColumnName(collection.Name) + ".id").
|
||||
AndWhere(dbx.In(app.ConcurrentDB().QuoteSimpleColumnName(collection.Name)+".id", recordIds...))
|
||||
|
||||
resolver := core.NewRecordFieldResolver(app, collection, requestInfo, true)
|
||||
expr, err := search.FilterData(*collection.ManageRule).BuildExpr(resolver)
|
||||
@@ -535,6 +537,27 @@ func firstApiError(errs ...error) *router.ApiError {
|
||||
return router.NewInternalServerError("", errors.Join(errs...))
|
||||
}
|
||||
|
||||
// execAfterSuccessTx ensures that fn is executed only after a succesul transaction.
|
||||
//
|
||||
// If the current app instance is not a transactional or checkTx is false,
|
||||
// then fn is directly executed.
|
||||
//
|
||||
// It could be usually used to allow propagating an error or writing
|
||||
// custom response from within the wrapped transaction block.
|
||||
func execAfterSuccessTx(checkTx bool, app core.App, fn func() error) error {
|
||||
if txInfo := app.TxInfo(); txInfo != nil && checkTx {
|
||||
txInfo.OnComplete(func(txErr error) error {
|
||||
if txErr == nil {
|
||||
return fn()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
return fn()
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const maxAuthOrigins = 5
|
||||
|
||||
+15
-11
@@ -153,13 +153,6 @@ func Serve(app core.App, config ServeConfig) error {
|
||||
ErrorLog: log.New(&serverErrorLogWriter{app: app}, "", 0),
|
||||
}
|
||||
|
||||
serveEvent := new(core.ServeEvent)
|
||||
serveEvent.App = app
|
||||
serveEvent.Router = pbRouter
|
||||
serveEvent.Server = server
|
||||
serveEvent.CertManager = certManager
|
||||
serveEvent.InstallerFunc = DefaultInstallerFunc
|
||||
|
||||
var listener net.Listener
|
||||
|
||||
// graceful shutdown
|
||||
@@ -207,6 +200,13 @@ func Serve(app core.App, config ServeConfig) error {
|
||||
|
||||
var baseURL string
|
||||
|
||||
serveEvent := new(core.ServeEvent)
|
||||
serveEvent.App = app
|
||||
serveEvent.Router = pbRouter
|
||||
serveEvent.Server = server
|
||||
serveEvent.CertManager = certManager
|
||||
serveEvent.InstallerFunc = DefaultInstallerFunc
|
||||
|
||||
// trigger the OnServe hook and start the tcp listener
|
||||
serveHookErr := app.OnServe().Trigger(serveEvent, func(e *core.ServeEvent) error {
|
||||
handler, err := e.Router.BuildMux()
|
||||
@@ -237,9 +237,13 @@ func Serve(app core.App, config ServeConfig) error {
|
||||
}
|
||||
}
|
||||
|
||||
listener, err = net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
if e.Listener == nil {
|
||||
listener, err = net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
listener = e.Listener
|
||||
}
|
||||
|
||||
if e.InstallerFunc != nil {
|
||||
@@ -260,7 +264,7 @@ func Serve(app core.App, config ServeConfig) error {
|
||||
|
||||
if listener == nil {
|
||||
//nolint:staticcheck
|
||||
return errors.New("The OnServe finalizer wasn't invoked. Did you forget to call the ServeEvent.Next() method?")
|
||||
return errors.New("The OnServe listener was not initialized. Did you forget to call the ServeEvent.Next() method?")
|
||||
}
|
||||
|
||||
if config.ShowStartBanner {
|
||||
|
||||
+6
-2
@@ -30,7 +30,9 @@ func settingsList(e *core.RequestEvent) error {
|
||||
event.Settings = clone
|
||||
|
||||
return e.App.OnSettingsListRequest().Trigger(event, func(e *core.SettingsListRequestEvent) error {
|
||||
return e.JSON(http.StatusOK, e.Settings)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.JSON(http.StatusOK, e.Settings)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -65,7 +67,9 @@ func settingsSet(e *core.RequestEvent) error {
|
||||
return e.InternalServerError("Failed to clone app settings.", err)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, appSettings)
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
return e.JSON(http.StatusOK, appSettings)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
)
|
||||
|
||||
@@ -58,6 +59,32 @@ func TestSettingsList(t *testing.T) {
|
||||
"OnSettingsListRequest": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnSettingsListRequest tx body write check",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/settings",
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnSettingsListRequest().BindFunc(func(e *core.SettingsListRequestEvent) error {
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedEvents: map[string]int{"OnSettingsListRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
@@ -176,6 +203,33 @@ func TestSettingsSet(t *testing.T) {
|
||||
"OnSettingsReload": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OnSettingsUpdateRequest tx body write check",
|
||||
Method: http.MethodPatch,
|
||||
URL: "/api/settings",
|
||||
Body: strings.NewReader(validData),
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.OnSettingsUpdateRequest().BindFunc(func(e *core.SettingsUpdateRequestEvent) error {
|
||||
original := e.App
|
||||
return e.App.RunInTransaction(func(txApp core.App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = original }()
|
||||
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.BadRequestError("TX_ERROR", nil)
|
||||
})
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedEvents: map[string]int{"OnSettingsUpdateRequest": 1},
|
||||
ExpectedContent: []string{"TX_ERROR"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
|
||||
+59
-19
@@ -45,6 +45,12 @@ type App interface {
|
||||
// IsTransactional checks if the current app instance is part of a transaction.
|
||||
IsTransactional() bool
|
||||
|
||||
// TxInfo returns the transaction associated with the current app instance (if any).
|
||||
//
|
||||
// Could be used if you want to execute indirectly a function after
|
||||
// the related app transaction completes using `app.TxInfo().OnAfterFunc(callback)`.
|
||||
TxInfo() *TxAppInfo
|
||||
|
||||
// Bootstrap initializes the application
|
||||
// (aka. create data dir, open db connections, load settings, etc.).
|
||||
//
|
||||
@@ -140,46 +146,82 @@ type App interface {
|
||||
// DB methods
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
// DB returns the default app data db instance (pb_data/data.db).
|
||||
// DB returns the default app data.db builder instance.
|
||||
//
|
||||
// To minimize SQLITE_BUSY errors, it automatically routes the
|
||||
// SELECT queries to the underlying concurrent db pool and everything else
|
||||
// to the nonconcurrent one.
|
||||
//
|
||||
// For more finer control over the used connections pools you can
|
||||
// call directly ConcurrentDB() or NonconcurrentDB().
|
||||
DB() dbx.Builder
|
||||
|
||||
// NonconcurrentDB returns the nonconcurrent app data db instance (pb_data/data.db).
|
||||
// ConcurrentDB returns the concurrent app data.db builder instance.
|
||||
//
|
||||
// This method is used mainly internally for executing db read
|
||||
// operations in a concurrent/non-blocking manner.
|
||||
//
|
||||
// Most users should use simply DB() as it will automatically
|
||||
// route the query execution to ConcurrentDB() or NonconcurrentDB().
|
||||
//
|
||||
// In a transaction the ConcurrentDB() and NonconcurrentDB() refer to the same *dbx.TX instance.
|
||||
ConcurrentDB() dbx.Builder
|
||||
|
||||
// NonconcurrentDB returns the nonconcurrent app data.db builder instance.
|
||||
//
|
||||
// The returned db instance is limited only to a single open connection,
|
||||
// meaning that it can process only 1 db operation at a time (other operations will be queued up).
|
||||
// meaning that it can process only 1 db operation at a time (other queries queue up).
|
||||
//
|
||||
// This method is used mainly internally and in the tests to execute write
|
||||
// (save/delete) db operations as it helps with minimizing the SQLITE_BUSY errors.
|
||||
//
|
||||
// For the majority of cases you would want to use the regular DB() method
|
||||
// since it allows concurrent db read operations.
|
||||
// Most users should use simply DB() as it will automatically
|
||||
// route the query execution to ConcurrentDB() or NonconcurrentDB().
|
||||
//
|
||||
// In a transaction the ConcurrentDB() and NonconcurrentDB() refer to the same *dbx.TX instance.
|
||||
NonconcurrentDB() dbx.Builder
|
||||
|
||||
// AuxDB returns the default app auxiliary db instance (pb_data/auxiliary.db).
|
||||
// AuxDB returns the app auxiliary.db builder instance.
|
||||
//
|
||||
// To minimize SQLITE_BUSY errors, it automatically routes the
|
||||
// SELECT queries to the underlying concurrent db pool and everything else
|
||||
// to the nonconcurrent one.
|
||||
//
|
||||
// For more finer control over the used connections pools you can
|
||||
// call directly AuxConcurrentDB() or AuxNonconcurrentDB().
|
||||
AuxDB() dbx.Builder
|
||||
|
||||
// AuxNonconcurrentDB returns the nonconcurrent app auxiliary db instance (pb_data/auxiliary.db)..
|
||||
// AuxConcurrentDB returns the concurrent app auxiliary.db builder instance.
|
||||
//
|
||||
// This method is used mainly internally for executing db read
|
||||
// operations in a concurrent/non-blocking manner.
|
||||
//
|
||||
// Most users should use simply AuxDB() as it will automatically
|
||||
// route the query execution to AuxConcurrentDB() or AuxNonconcurrentDB().
|
||||
//
|
||||
// In a transaction the AuxConcurrentDB() and AuxNonconcurrentDB() refer to the same *dbx.TX instance.
|
||||
AuxConcurrentDB() dbx.Builder
|
||||
|
||||
// AuxNonconcurrentDB returns the nonconcurrent app auxiliary.db builder instance.
|
||||
//
|
||||
// The returned db instance is limited only to a single open connection,
|
||||
// meaning that it can process only 1 db operation at a time (other operations will be queued up).
|
||||
// meaning that it can process only 1 db operation at a time (other queries queue up).
|
||||
//
|
||||
// This method is used mainly internally and in the tests to execute write
|
||||
// (save/delete) db operations as it helps with minimizing the SQLITE_BUSY errors.
|
||||
//
|
||||
// For the majority of cases you would want to use the regular DB() method
|
||||
// since it allows concurrent db read operations.
|
||||
// Most users should use simply AuxDB() as it will automatically
|
||||
// route the query execution to AuxConcurrentDB() or AuxNonconcurrentDB().
|
||||
//
|
||||
// In a transaction the AuxNonconcurrentDB() and AuxNonconcurrentDB() refer to the same *dbx.TX instance.
|
||||
// In a transaction the AuxConcurrentDB() and AuxNonconcurrentDB() refer to the same *dbx.TX instance.
|
||||
AuxNonconcurrentDB() dbx.Builder
|
||||
|
||||
// HasTable checks if a table (or view) with the provided name exists (case insensitive).
|
||||
// in the current app.DB() instance.
|
||||
// in the data.db.
|
||||
HasTable(tableName string) bool
|
||||
|
||||
// AuxHasTable checks if a table (or view) with the provided name exists (case insensitive)
|
||||
// in the current app.AuxDB() instance.
|
||||
// in the auxiliary.db.
|
||||
AuxHasTable(tableName string) bool
|
||||
|
||||
// TableColumns returns all column names of a single table by its name.
|
||||
@@ -225,21 +267,19 @@ type App interface {
|
||||
// FindRecordByViewFile returns the original Record of the provided view collection file.
|
||||
FindRecordByViewFile(viewCollectionModelOrIdentifier any, fileFieldName string, filename string) (*Record, error)
|
||||
|
||||
// Vacuum executes VACUUM on the current app.DB() instance
|
||||
// in order to reclaim unused data db disk space.
|
||||
// Vacuum executes VACUUM on the data.db in order to reclaim unused data db disk space.
|
||||
Vacuum() error
|
||||
|
||||
// AuxVacuum executes VACUUM on the current app.AuxDB() instance
|
||||
// in order to reclaim unused auxiliary db disk space.
|
||||
// AuxVacuum executes VACUUM on the auxiliary.db in order to reclaim unused auxiliary db disk space.
|
||||
AuxVacuum() error
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
// ModelQuery creates a new preconfigured select app.DB() query with preset
|
||||
// ModelQuery creates a new preconfigured select data.db query with preset
|
||||
// SELECT, FROM and other common fields based on the provided model.
|
||||
ModelQuery(model Model) *dbx.SelectQuery
|
||||
|
||||
// AuxModelQuery creates a new preconfigured select app.AuxDB() query with preset
|
||||
// AuxModelQuery creates a new preconfigured select auxiliary.db query with preset
|
||||
// SELECT, FROM and other common fields based on the provided model.
|
||||
AuxModelQuery(model Model) *dbx.SelectQuery
|
||||
|
||||
|
||||
+82
-18
@@ -71,7 +71,7 @@ var _ App = (*BaseApp)(nil)
|
||||
// BaseApp implements core.App and defines the base PocketBase app structure.
|
||||
type BaseApp struct {
|
||||
config *BaseAppConfig
|
||||
txInfo *txAppInfo
|
||||
txInfo *TxAppInfo
|
||||
store *store.Store[string, any]
|
||||
cron *cron.Cron
|
||||
settings *Settings
|
||||
@@ -362,9 +362,17 @@ func (app *BaseApp) Logger() *slog.Logger {
|
||||
return app.logger
|
||||
}
|
||||
|
||||
// TxInfo returns the transaction associated with the current app instance (if any).
|
||||
//
|
||||
// Could be used if you want to execute indirectly a function after
|
||||
// the related app transaction completes using `app.TxInfo().OnAfterFunc(callback)`.
|
||||
func (app *BaseApp) TxInfo() *TxAppInfo {
|
||||
return app.txInfo
|
||||
}
|
||||
|
||||
// IsTransactional checks if the current app instance is part of a transaction.
|
||||
func (app *BaseApp) IsTransactional() bool {
|
||||
return app.txInfo != nil
|
||||
return app.TxInfo() != nil
|
||||
}
|
||||
|
||||
// IsBootstrapped checks if the application was initialized
|
||||
@@ -468,44 +476,100 @@ func (app *BaseApp) ResetBootstrapState() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DB returns the default app data db instance (pb_data/data.db).
|
||||
// DB returns the default app data.db builder instance.
|
||||
//
|
||||
// To minimize SQLITE_BUSY errors, it automatically routes the
|
||||
// SELECT queries to the underlying concurrent db pool and everything
|
||||
// else to the nonconcurrent one.
|
||||
//
|
||||
// For more finer control over the used connections pools you can
|
||||
// call directly ConcurrentDB() or NonconcurrentDB().
|
||||
func (app *BaseApp) DB() dbx.Builder {
|
||||
// transactional or both are nil
|
||||
if app.concurrentDB == app.nonconcurrentDB {
|
||||
return app.concurrentDB
|
||||
}
|
||||
|
||||
return &dualDBBuilder{
|
||||
concurrentDB: app.concurrentDB,
|
||||
nonconcurrentDB: app.nonconcurrentDB,
|
||||
}
|
||||
}
|
||||
|
||||
// ConcurrentDB returns the concurrent app data.db builder instance.
|
||||
//
|
||||
// This method is used mainly internally for executing db read
|
||||
// operations in a concurrent/non-blocking manner.
|
||||
//
|
||||
// Most users should use simply DB() as it will automatically
|
||||
// route the query execution to ConcurrentDB() or NonconcurrentDB().
|
||||
//
|
||||
// In a transaction the ConcurrentDB() and NonconcurrentDB() refer to the same *dbx.TX instance.
|
||||
func (app *BaseApp) ConcurrentDB() dbx.Builder {
|
||||
return app.concurrentDB
|
||||
}
|
||||
|
||||
// NonconcurrentDB returns the nonconcurrent app data db instance (pb_data/data.db).
|
||||
// NonconcurrentDB returns the nonconcurrent app data.db builder instance.
|
||||
//
|
||||
// The returned db instance is limited only to a single open connection,
|
||||
// meaning that it can process only 1 db operation at a time (other operations will be queued up).
|
||||
// meaning that it can process only 1 db operation at a time (other queries queue up).
|
||||
//
|
||||
// This method is used mainly internally and in the tests to execute write
|
||||
// (save/delete) db operations as it helps with minimizing the SQLITE_BUSY errors.
|
||||
//
|
||||
// For the majority of cases you would want to use the regular DB() method
|
||||
// since it allows concurrent db read operations.
|
||||
// Most users should use simply DB() as it will automatically
|
||||
// route the query execution to ConcurrentDB() or NonconcurrentDB().
|
||||
//
|
||||
// In a transaction the ConcurrentDB() and NonconcurrentDB() refer to the same *dbx.TX instance.
|
||||
func (app *BaseApp) NonconcurrentDB() dbx.Builder {
|
||||
return app.nonconcurrentDB
|
||||
}
|
||||
|
||||
// AuxDB returns the default app auxiliary db instance (pb_data/auxiliary.db).
|
||||
// AuxDB returns the app auxiliary.db builder instance.
|
||||
//
|
||||
// To minimize SQLITE_BUSY errors, it automatically routes the
|
||||
// SELECT queries to the underlying concurrent db pool and everything
|
||||
// else to the nonconcurrent one.
|
||||
//
|
||||
// For more finer control over the used connections pools you can
|
||||
// call directly AuxConcurrentDB() or AuxNonconcurrentDB().
|
||||
func (app *BaseApp) AuxDB() dbx.Builder {
|
||||
// transactional or both are nil
|
||||
if app.auxConcurrentDB == app.auxNonconcurrentDB {
|
||||
return app.auxConcurrentDB
|
||||
}
|
||||
|
||||
return &dualDBBuilder{
|
||||
concurrentDB: app.auxConcurrentDB,
|
||||
nonconcurrentDB: app.auxNonconcurrentDB,
|
||||
}
|
||||
}
|
||||
|
||||
// AuxConcurrentDB returns the concurrent app auxiliary.db builder instance.
|
||||
//
|
||||
// This method is used mainly internally for executing db read
|
||||
// operations in a concurrent/non-blocking manner.
|
||||
//
|
||||
// Most users should use simply AuxDB() as it will automatically
|
||||
// route the query execution to AuxConcurrentDB() or AuxNonconcurrentDB().
|
||||
//
|
||||
// In a transaction the AuxConcurrentDB() and AuxNonconcurrentDB() refer to the same *dbx.TX instance.
|
||||
func (app *BaseApp) AuxConcurrentDB() dbx.Builder {
|
||||
return app.auxConcurrentDB
|
||||
}
|
||||
|
||||
// AuxNonconcurrentDB returns the nonconcurrent app auxiliary db instance (pb_data/auxiliary.db).
|
||||
// AuxNonconcurrentDB returns the nonconcurrent app auxiliary.db builder instance.
|
||||
//
|
||||
// The returned db instance is limited only to a single open connection,
|
||||
// meaning that it can process only 1 db operation at a time (other operations will be queued up).
|
||||
// meaning that it can process only 1 db operation at a time (other queries queue up).
|
||||
//
|
||||
// This method is used mainly internally and in the tests to execute write
|
||||
// (save/delete) db operations as it helps with minimizing the SQLITE_BUSY errors.
|
||||
//
|
||||
// For the majority of cases you would want to use the regular DB() method
|
||||
// since it allows concurrent db read operations.
|
||||
// Most users should use simply AuxDB() as it will automatically
|
||||
// route the query execution to AuxConcurrentDB() or AuxNonconcurrentDB().
|
||||
//
|
||||
// In a transaction the AuxNonconcurrentDB() and AuxNonconcurrentDB() refer to the same *dbx.TX instance.
|
||||
// In a transaction the AuxConcurrentDB() and AuxNonconcurrentDB() refer to the same *dbx.TX instance.
|
||||
func (app *BaseApp) AuxNonconcurrentDB() dbx.Builder {
|
||||
return app.auxNonconcurrentDB
|
||||
}
|
||||
@@ -1147,10 +1211,10 @@ var sqlLogReplacements = []struct {
|
||||
}{
|
||||
{regexp.MustCompile(`\[\[([^\[\]\{\}\.]+)\.([^\[\]\{\}\.]+)\]\]`), "`$1`.`$2`"},
|
||||
{regexp.MustCompile(`\{\{([^\[\]\{\}\.]+)\.([^\[\]\{\}\.]+)\}\}`), "`$1`.`$2`"},
|
||||
{regexp.MustCompile(`([^'"])\{\{`), "$1`"},
|
||||
{regexp.MustCompile(`\}\}([^'"])`), "`$1"},
|
||||
{regexp.MustCompile(`([^'"])\[\[`), "$1`"},
|
||||
{regexp.MustCompile(`\]\]([^'"])`), "`$1"},
|
||||
{regexp.MustCompile(`([^'"])?\{\{`), "$1`"},
|
||||
{regexp.MustCompile(`\}\}([^'"])?`), "`$1"},
|
||||
{regexp.MustCompile(`([^'"])?\[\[`), "$1`"},
|
||||
{regexp.MustCompile(`\]\]([^'"])?`), "`$1"},
|
||||
{regexp.MustCompile(`<nil>`), "NULL"},
|
||||
}
|
||||
|
||||
@@ -1301,7 +1365,7 @@ func (app *BaseApp) registerBaseHooks() {
|
||||
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()
|
||||
_, execErr = app.NonconcurrentDB().NewQuery("PRAGMA optimize").Execute()
|
||||
if execErr != nil {
|
||||
app.Logger().Warn("Failed to run periodic PRAGMA optimize", slog.String("error", execErr.Error()))
|
||||
}
|
||||
|
||||
+33
-19
@@ -71,7 +71,7 @@ func (app *BaseApp) CreateBackup(ctx context.Context, name string) error {
|
||||
|
||||
// archive pb_data in a temp directory, exluding the "backups" and the temp dirs
|
||||
//
|
||||
// Run in transaction to temporary block other writes (transactions uses the NonconcurrentDB connection).
|
||||
// run in transaction to temporary block other writes (transactions uses the NonconcurrentDB connection)
|
||||
// ---
|
||||
tempPath := filepath.Join(localTempDir, "pb_backup_"+security.PseudorandomString(6))
|
||||
createErr := e.App.RunInTransaction(func(txApp App) error {
|
||||
@@ -190,7 +190,7 @@ func (app *BaseApp) RestoreBackup(ctx context.Context, name string) error {
|
||||
|
||||
// extract the zip
|
||||
if e.App.Settings().Backups.S3.Enabled {
|
||||
br, err := fsys.GetFile(name)
|
||||
br, err := fsys.GetReader(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -228,7 +228,7 @@ func (app *BaseApp) RestoreBackup(ctx context.Context, name string) error {
|
||||
} else {
|
||||
// manually construct the local path to avoid creating a copy of the zip file
|
||||
// since the blob reader currently doesn't implement ReaderAt
|
||||
zipPath := filepath.Join(app.DataDir(), LocalBackupsDirName, filepath.Base(name))
|
||||
zipPath := filepath.Join(e.App.DataDir(), LocalBackupsDirName, filepath.Base(name))
|
||||
|
||||
err = archive.Extract(zipPath, extractedDataDir)
|
||||
if err != nil {
|
||||
@@ -242,29 +242,43 @@ func (app *BaseApp) RestoreBackup(ctx context.Context, name string) error {
|
||||
return fmt.Errorf("data.db file is missing or invalid: %w", err)
|
||||
}
|
||||
|
||||
// move the current pb_data content to a special temp location
|
||||
// that will hold the old data between dirs replace
|
||||
// (the temp dir will be automatically removed on the next app start)
|
||||
oldTempDataDir := filepath.Join(localTempDir, "old_pb_data_"+security.PseudorandomString(8))
|
||||
if err := osutils.MoveDirContent(e.App.DataDir(), oldTempDataDir, e.Exclude...); err != nil {
|
||||
return fmt.Errorf("failed to move the current pb_data content to a temp location: %w", err)
|
||||
}
|
||||
|
||||
// move the extracted archive content to the app's pb_data
|
||||
if err := osutils.MoveDirContent(extractedDataDir, e.App.DataDir(), e.Exclude...); err != nil {
|
||||
return fmt.Errorf("failed to move the extracted archive content to pb_data: %w", err)
|
||||
replaceErr := e.App.RunInTransaction(func(txApp App) error {
|
||||
return txApp.AuxRunInTransaction(func(txApp App) error {
|
||||
// move the current pb_data content to a special temp location
|
||||
// that will hold the old data between dirs replace
|
||||
// (the temp dir will be automatically removed on the next app start)
|
||||
if err := osutils.MoveDirContent(txApp.DataDir(), oldTempDataDir, e.Exclude...); err != nil {
|
||||
return fmt.Errorf("failed to move the current pb_data content to a temp location: %w", err)
|
||||
}
|
||||
|
||||
// move the extracted archive content to the app's pb_data
|
||||
if err := osutils.MoveDirContent(extractedDataDir, txApp.DataDir(), e.Exclude...); err != nil {
|
||||
return fmt.Errorf("failed to move the extracted archive content to pb_data: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if replaceErr != nil {
|
||||
return replaceErr
|
||||
}
|
||||
|
||||
revertDataDirChanges := func() error {
|
||||
if err := osutils.MoveDirContent(e.App.DataDir(), extractedDataDir, e.Exclude...); err != nil {
|
||||
return fmt.Errorf("failed to revert the extracted dir change: %w", err)
|
||||
}
|
||||
return e.App.RunInTransaction(func(txApp App) error {
|
||||
return txApp.AuxRunInTransaction(func(txApp App) error {
|
||||
if err := osutils.MoveDirContent(txApp.DataDir(), extractedDataDir, e.Exclude...); err != nil {
|
||||
return fmt.Errorf("failed to revert the extracted dir change: %w", err)
|
||||
}
|
||||
|
||||
if err := osutils.MoveDirContent(oldTempDataDir, e.App.DataDir(), e.Exclude...); err != nil {
|
||||
return fmt.Errorf("failed to revert old pb_data dir change: %w", err)
|
||||
}
|
||||
if err := osutils.MoveDirContent(oldTempDataDir, txApp.DataDir(), e.Exclude...); err != nil {
|
||||
return fmt.Errorf("failed to revert old pb_data dir change: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// restart the app
|
||||
|
||||
+185
-11
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
"os"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -99,9 +100,11 @@ func TestBaseAppBootstrap(t *testing.T) {
|
||||
}
|
||||
|
||||
nilChecksBeforeReset := []nilCheck{
|
||||
{"[before] concurrentDB", app.DB(), false},
|
||||
{"[before] db", app.DB(), false},
|
||||
{"[before] concurrentDB", app.ConcurrentDB(), false},
|
||||
{"[before] nonconcurrentDB", app.NonconcurrentDB(), false},
|
||||
{"[before] auxConcurrentDB", app.AuxDB(), false},
|
||||
{"[before] auxDB", app.AuxDB(), false},
|
||||
{"[before] auxConcurrentDB", app.AuxConcurrentDB(), false},
|
||||
{"[before] auxNonconcurrentDB", app.AuxNonconcurrentDB(), false},
|
||||
{"[before] settings", app.Settings(), false},
|
||||
{"[before] logger", app.Logger(), false},
|
||||
@@ -116,9 +119,11 @@ func TestBaseAppBootstrap(t *testing.T) {
|
||||
}
|
||||
|
||||
nilChecksAfterReset := []nilCheck{
|
||||
{"[after] concurrentDB", app.DB(), true},
|
||||
{"[after] db", app.DB(), true},
|
||||
{"[after] concurrentDB", app.ConcurrentDB(), true},
|
||||
{"[after] nonconcurrentDB", app.NonconcurrentDB(), true},
|
||||
{"[after] auxConcurrentDB", app.AuxDB(), true},
|
||||
{"[after] auxDB", app.AuxDB(), true},
|
||||
{"[after] auxConcurrentDB", app.AuxConcurrentDB(), true},
|
||||
{"[after] auxNonconcurrentDB", app.AuxNonconcurrentDB(), true},
|
||||
{"[after] settings", app.Settings(), false},
|
||||
{"[after] logger", app.Logger(), false},
|
||||
@@ -128,7 +133,7 @@ func TestBaseAppBootstrap(t *testing.T) {
|
||||
runNilChecks(nilChecksAfterReset)
|
||||
}
|
||||
|
||||
func TestNewBaseAppIsTransactional(t *testing.T) {
|
||||
func TestNewBaseAppTx(t *testing.T) {
|
||||
const testDataDir = "./pb_base_app_test_data_dir/"
|
||||
defer os.RemoveAll(testDataDir)
|
||||
|
||||
@@ -141,17 +146,34 @@ func TestNewBaseAppIsTransactional(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if app.IsTransactional() {
|
||||
t.Fatalf("Didn't expect the app to be transactional")
|
||||
mustNotHaveTx := func(app core.App) {
|
||||
if app.IsTransactional() {
|
||||
t.Fatalf("Didn't expect the app to be transactional")
|
||||
}
|
||||
|
||||
if app.TxInfo() != nil {
|
||||
t.Fatalf("Didn't expect the app.txInfo to be loaded")
|
||||
}
|
||||
}
|
||||
|
||||
app.RunInTransaction(func(txApp core.App) error {
|
||||
if !txApp.IsTransactional() {
|
||||
mustHaveTx := func(app core.App) {
|
||||
if !app.IsTransactional() {
|
||||
t.Fatalf("Expected the app to be transactional")
|
||||
}
|
||||
|
||||
if app.TxInfo() == nil {
|
||||
t.Fatalf("Expected the app.txInfo to be loaded")
|
||||
}
|
||||
}
|
||||
|
||||
mustNotHaveTx(app)
|
||||
|
||||
app.RunInTransaction(func(txApp core.App) error {
|
||||
mustHaveTx(txApp)
|
||||
return nil
|
||||
})
|
||||
|
||||
mustNotHaveTx(app)
|
||||
}
|
||||
|
||||
func TestBaseAppNewMailClient(t *testing.T) {
|
||||
@@ -354,8 +376,8 @@ func TestBaseAppRefreshSettingsLoggerMinLevelEnabled(t *testing.T) {
|
||||
}
|
||||
|
||||
// silence query logs
|
||||
app.DB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {}
|
||||
app.DB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {}
|
||||
app.ConcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {}
|
||||
app.ConcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {}
|
||||
app.NonconcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {}
|
||||
app.NonconcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {}
|
||||
|
||||
@@ -378,3 +400,155 @@ func TestBaseAppRefreshSettingsLoggerMinLevelEnabled(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseAppDBDualBuilder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
concurrentQueries := []string{}
|
||||
nonconcurrentQueries := []string{}
|
||||
app.ConcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
concurrentQueries = append(concurrentQueries, sql)
|
||||
}
|
||||
app.ConcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
||||
concurrentQueries = append(concurrentQueries, sql)
|
||||
}
|
||||
app.NonconcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
nonconcurrentQueries = append(nonconcurrentQueries, sql)
|
||||
}
|
||||
app.NonconcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
||||
nonconcurrentQueries = append(nonconcurrentQueries, sql)
|
||||
}
|
||||
|
||||
type testQuery struct {
|
||||
query string
|
||||
isConcurrent bool
|
||||
}
|
||||
|
||||
regularTests := []testQuery{
|
||||
{" \n sEleCt 1", true},
|
||||
{"With abc(x) AS (select 2) SELECT x FROM abc", true},
|
||||
{"create table t1(x int)", false},
|
||||
{"insert into t1(x) values(1)", false},
|
||||
{"update t1 set x = 2", false},
|
||||
{"delete from t1", false},
|
||||
}
|
||||
|
||||
txTests := []testQuery{
|
||||
{"select 3", false},
|
||||
{" \n WITH abc(x) AS (select 4) SELECT x FROM abc", false},
|
||||
{"create table t2(x int)", false},
|
||||
{"insert into t2(x) values(1)", false},
|
||||
{"update t2 set x = 2", false},
|
||||
{"delete from t2", false},
|
||||
}
|
||||
|
||||
for _, item := range regularTests {
|
||||
_, err := app.DB().NewQuery(item.query).Execute()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute query %q error: %v", item.query, err)
|
||||
}
|
||||
}
|
||||
|
||||
app.RunInTransaction(func(txApp core.App) error {
|
||||
for _, item := range txTests {
|
||||
_, err := txApp.DB().NewQuery(item.query).Execute()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute query %q error: %v", item.query, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
allTests := append(regularTests, txTests...)
|
||||
for _, item := range allTests {
|
||||
if item.isConcurrent {
|
||||
if !slices.Contains(concurrentQueries, item.query) {
|
||||
t.Fatalf("Expected concurrent query\n%q\ngot\nconcurrent:%v\nnonconcurrent:%v", item.query, concurrentQueries, nonconcurrentQueries)
|
||||
}
|
||||
} else {
|
||||
if !slices.Contains(nonconcurrentQueries, item.query) {
|
||||
t.Fatalf("Expected nonconcurrent query\n%q\ngot\nconcurrent:%v\nnonconcurrent:%v", item.query, concurrentQueries, nonconcurrentQueries)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseAppAuxDBDualBuilder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
concurrentQueries := []string{}
|
||||
nonconcurrentQueries := []string{}
|
||||
app.AuxConcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
concurrentQueries = append(concurrentQueries, sql)
|
||||
}
|
||||
app.AuxConcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
||||
concurrentQueries = append(concurrentQueries, sql)
|
||||
}
|
||||
app.AuxNonconcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
nonconcurrentQueries = append(nonconcurrentQueries, sql)
|
||||
}
|
||||
app.AuxNonconcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
||||
nonconcurrentQueries = append(nonconcurrentQueries, sql)
|
||||
}
|
||||
|
||||
type testQuery struct {
|
||||
query string
|
||||
isConcurrent bool
|
||||
}
|
||||
|
||||
regularTests := []testQuery{
|
||||
{" \n sEleCt 1", true},
|
||||
{"With abc(x) AS (select 2) SELECT x FROM abc", true},
|
||||
{"create table t1(x int)", false},
|
||||
{"insert into t1(x) values(1)", false},
|
||||
{"update t1 set x = 2", false},
|
||||
{"delete from t1", false},
|
||||
}
|
||||
|
||||
txTests := []testQuery{
|
||||
{"select 3", false},
|
||||
{" \n WITH abc(x) AS (select 4) SELECT x FROM abc", false},
|
||||
{"create table t2(x int)", false},
|
||||
{"insert into t2(x) values(1)", false},
|
||||
{"update t2 set x = 2", false},
|
||||
{"delete from t2", false},
|
||||
}
|
||||
|
||||
for _, item := range regularTests {
|
||||
_, err := app.AuxDB().NewQuery(item.query).Execute()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute query %q error: %v", item.query, err)
|
||||
}
|
||||
}
|
||||
|
||||
app.AuxRunInTransaction(func(txApp core.App) error {
|
||||
for _, item := range txTests {
|
||||
_, err := txApp.AuxDB().NewQuery(item.query).Execute()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute query %q error: %v", item.query, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
allTests := append(regularTests, txTests...)
|
||||
for _, item := range allTests {
|
||||
if item.isConcurrent {
|
||||
if !slices.Contains(concurrentQueries, item.query) {
|
||||
t.Fatalf("Expected concurrent query\n%q\ngot\nconcurrent:%v\nnonconcurrent:%v", item.query, concurrentQueries, nonconcurrentQueries)
|
||||
}
|
||||
} else {
|
||||
if !slices.Contains(nonconcurrentQueries, item.query) {
|
||||
t.Fatalf("Expected nonconcurrent query\n%q\ngot\nconcurrent:%v\nnonconcurrent:%v", item.query, concurrentQueries, nonconcurrentQueries)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1623,7 +1623,7 @@ func TestCollectionSaveViewWrapping(t *testing.T) {
|
||||
|
||||
var sql string
|
||||
|
||||
rowErr := app.DB().NewQuery("SELECT sql FROM sqlite_master WHERE type='view' AND name={:name}").
|
||||
rowErr := app.ConcurrentDB().NewQuery("SELECT sql FROM sqlite_master WHERE type='view' AND name={:name}").
|
||||
Bind(dbx.Params{"name": viewName}).
|
||||
Row(&sql)
|
||||
if rowErr != nil {
|
||||
|
||||
@@ -38,7 +38,7 @@ func (app *BaseApp) FindAllCollections(collectionTypes ...string) ([]*Collection
|
||||
q.AndWhere(dbx.In("type", list.ToInterfaceSlice(types)...))
|
||||
}
|
||||
|
||||
err := q.OrderBy("created ASC").All(&collections)
|
||||
err := q.OrderBy("rowid ASC").All(&collections)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ func TestFindCachedCollectionByNameOrId(t *testing.T) {
|
||||
defer app.Cleanup()
|
||||
|
||||
totalQueries := 0
|
||||
app.DB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
app.ConcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
totalQueries++
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ func TestFindCachedCollectionReferences(t *testing.T) {
|
||||
}
|
||||
|
||||
totalQueries := 0
|
||||
app.DB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
app.ConcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
totalQueries++
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ func (app *BaseApp) SyncRecordTableSchema(newCollection *Collection, oldCollecti
|
||||
|
||||
// run optimize per the SQLite recommendations
|
||||
// (https://www.sqlite.org/pragma.html#pragma_optimize)
|
||||
_, optimizeErr := app.DB().NewQuery("PRAGMA optimize").Execute()
|
||||
_, optimizeErr := app.NonconcurrentDB().NewQuery("PRAGMA optimize").Execute()
|
||||
if optimizeErr != nil {
|
||||
app.Logger().Warn("Failed to run PRAGMA optimize after record table sync", slog.String("error", optimizeErr.Error()))
|
||||
}
|
||||
@@ -310,7 +310,8 @@ func dropCollectionIndexes(app App, collection *Collection) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := app.DB().NewQuery(fmt.Sprintf("DROP INDEX IF EXISTS [[%s]]", parsed.IndexName)).Execute(); err != nil {
|
||||
_, err := txApp.DB().NewQuery(fmt.Sprintf("DROP INDEX IF EXISTS [[%s]]", parsed.IndexName)).Execute()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ func (validator *collectionValidator) run() error {
|
||||
validator.original.IsNew(),
|
||||
validation.Length(1, 100),
|
||||
validation.Match(DefaultIdRegex),
|
||||
validation.By(validators.UniqueId(validator.app.DB(), validator.new.TableName())),
|
||||
validation.By(validators.UniqueId(validator.app.ConcurrentDB(), validator.new.TableName())),
|
||||
).Else(
|
||||
validation.By(validators.Equal(validator.original.Id)),
|
||||
),
|
||||
@@ -558,7 +558,7 @@ func (cv *collectionValidator) checkIndexes(value any) error {
|
||||
|
||||
// ensure that the index name is not used in another collection
|
||||
var usedTblName string
|
||||
_ = cv.app.DB().Select("tbl_name").
|
||||
_ = cv.app.ConcurrentDB().Select("tbl_name").
|
||||
From("sqlite_master").
|
||||
AndWhere(dbx.HashExp{"type": "index"}).
|
||||
AndWhere(dbx.NewExp("LOWER([[tbl_name]])!=LOWER({:oldName})", dbx.Params{"oldName": cv.original.Name})).
|
||||
|
||||
+8
-8
@@ -62,16 +62,16 @@ func crc32Checksum(str string) string {
|
||||
return strconv.FormatInt(int64(crc32.ChecksumIEEE([]byte(str))), 10)
|
||||
}
|
||||
|
||||
// ModelQuery creates a new preconfigured select app.DB() query with preset
|
||||
// ModelQuery creates a new preconfigured select data.db query with preset
|
||||
// SELECT, FROM and other common fields based on the provided model.
|
||||
func (app *BaseApp) ModelQuery(m Model) *dbx.SelectQuery {
|
||||
return app.modelQuery(app.DB(), m)
|
||||
return app.modelQuery(app.ConcurrentDB(), m)
|
||||
}
|
||||
|
||||
// AuxModelQuery creates a new preconfigured select app.AuxDB() query with preset
|
||||
// AuxModelQuery creates a new preconfigured select auxiliary.db query with preset
|
||||
// SELECT, FROM and other common fields based on the provided model.
|
||||
func (app *BaseApp) AuxModelQuery(m Model) *dbx.SelectQuery {
|
||||
return app.modelQuery(app.AuxDB(), m)
|
||||
return app.modelQuery(app.AuxConcurrentDB(), m)
|
||||
}
|
||||
|
||||
func (app *BaseApp) modelQuery(db dbx.Builder, m Model) *dbx.SelectQuery {
|
||||
@@ -151,7 +151,7 @@ func (app *BaseApp) delete(ctx context.Context, model Model, isForAuxDB bool) er
|
||||
|
||||
if app.txInfo != nil {
|
||||
// execute later after the transaction has completed
|
||||
app.txInfo.onAfterFunc(func(txErr error) error {
|
||||
app.txInfo.OnComplete(func(txErr error) error {
|
||||
if app.txInfo != nil && app.txInfo.parent != nil {
|
||||
event.App = app.txInfo.parent
|
||||
}
|
||||
@@ -342,7 +342,7 @@ func (app *BaseApp) create(ctx context.Context, model Model, withValidations boo
|
||||
|
||||
if app.txInfo != nil {
|
||||
// execute later after the transaction has completed
|
||||
app.txInfo.onAfterFunc(func(txErr error) error {
|
||||
app.txInfo.OnComplete(func(txErr error) error {
|
||||
if app.txInfo != nil && app.txInfo.parent != nil {
|
||||
event.App = app.txInfo.parent
|
||||
}
|
||||
@@ -426,7 +426,7 @@ func (app *BaseApp) update(ctx context.Context, model Model, withValidations boo
|
||||
|
||||
if app.txInfo != nil {
|
||||
// execute later after the transaction has completed
|
||||
app.txInfo.onAfterFunc(func(txErr error) error {
|
||||
app.txInfo.OnComplete(func(txErr error) error {
|
||||
if app.txInfo != nil && app.txInfo.parent != nil {
|
||||
event.App = app.txInfo.parent
|
||||
}
|
||||
@@ -484,7 +484,7 @@ func validateRecordId(app App, collectionNameOrId string) validation.RuleFunc {
|
||||
|
||||
var exists int
|
||||
|
||||
rowErr := app.DB().Select("(1)").
|
||||
rowErr := app.ConcurrentDB().Select("(1)").
|
||||
From(collection.Name).
|
||||
AndWhere(dbx.HashExp{"id": id}).
|
||||
Limit(1).
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
||||
var _ dbx.Builder = (*dualDBBuilder)(nil)
|
||||
|
||||
// note: expects both builder to use the same driver
|
||||
type dualDBBuilder struct {
|
||||
concurrentDB dbx.Builder
|
||||
nonconcurrentDB dbx.Builder
|
||||
}
|
||||
|
||||
// Select implements the [dbx.Builder.Select] interface method.
|
||||
func (b *dualDBBuilder) Select(cols ...string) *dbx.SelectQuery {
|
||||
return b.concurrentDB.Select(cols...)
|
||||
}
|
||||
|
||||
// Model implements the [dbx.Builder.Model] interface method.
|
||||
func (b *dualDBBuilder) Model(data interface{}) *dbx.ModelQuery {
|
||||
return b.nonconcurrentDB.Model(data)
|
||||
}
|
||||
|
||||
// GeneratePlaceholder implements the [dbx.Builder.GeneratePlaceholder] interface method.
|
||||
func (b *dualDBBuilder) GeneratePlaceholder(i int) string {
|
||||
return b.concurrentDB.GeneratePlaceholder(i)
|
||||
}
|
||||
|
||||
// Quote implements the [dbx.Builder.Quote] interface method.
|
||||
func (b *dualDBBuilder) Quote(str string) string {
|
||||
return b.concurrentDB.Quote(str)
|
||||
}
|
||||
|
||||
// QuoteSimpleTableName implements the [dbx.Builder.QuoteSimpleTableName] interface method.
|
||||
func (b *dualDBBuilder) QuoteSimpleTableName(table string) string {
|
||||
return b.concurrentDB.QuoteSimpleTableName(table)
|
||||
}
|
||||
|
||||
// QuoteSimpleColumnName implements the [dbx.Builder.QuoteSimpleColumnName] interface method.
|
||||
func (b *dualDBBuilder) QuoteSimpleColumnName(col string) string {
|
||||
return b.concurrentDB.QuoteSimpleColumnName(col)
|
||||
}
|
||||
|
||||
// QueryBuilder implements the [dbx.Builder.QueryBuilder] interface method.
|
||||
func (b *dualDBBuilder) QueryBuilder() dbx.QueryBuilder {
|
||||
return b.concurrentDB.QueryBuilder()
|
||||
}
|
||||
|
||||
// Insert implements the [dbx.Builder.Insert] interface method.
|
||||
func (b *dualDBBuilder) Insert(table string, cols dbx.Params) *dbx.Query {
|
||||
return b.nonconcurrentDB.Insert(table, cols)
|
||||
}
|
||||
|
||||
// Upsert implements the [dbx.Builder.Upsert] interface method.
|
||||
func (b *dualDBBuilder) Upsert(table string, cols dbx.Params, constraints ...string) *dbx.Query {
|
||||
return b.nonconcurrentDB.Upsert(table, cols, constraints...)
|
||||
}
|
||||
|
||||
// Update implements the [dbx.Builder.Update] interface method.
|
||||
func (b *dualDBBuilder) Update(table string, cols dbx.Params, where dbx.Expression) *dbx.Query {
|
||||
return b.nonconcurrentDB.Update(table, cols, where)
|
||||
}
|
||||
|
||||
// Delete implements the [dbx.Builder.Delete] interface method.
|
||||
func (b *dualDBBuilder) Delete(table string, where dbx.Expression) *dbx.Query {
|
||||
return b.nonconcurrentDB.Delete(table, where)
|
||||
}
|
||||
|
||||
// CreateTable implements the [dbx.Builder.CreateTable] interface method.
|
||||
func (b *dualDBBuilder) CreateTable(table string, cols map[string]string, options ...string) *dbx.Query {
|
||||
return b.nonconcurrentDB.CreateTable(table, cols, options...)
|
||||
}
|
||||
|
||||
// RenameTable implements the [dbx.Builder.RenameTable] interface method.
|
||||
func (b *dualDBBuilder) RenameTable(oldName, newName string) *dbx.Query {
|
||||
return b.nonconcurrentDB.RenameTable(oldName, newName)
|
||||
}
|
||||
|
||||
// DropTable implements the [dbx.Builder.DropTable] interface method.
|
||||
func (b *dualDBBuilder) DropTable(table string) *dbx.Query {
|
||||
return b.nonconcurrentDB.DropTable(table)
|
||||
}
|
||||
|
||||
// TruncateTable implements the [dbx.Builder.TruncateTable] interface method.
|
||||
func (b *dualDBBuilder) TruncateTable(table string) *dbx.Query {
|
||||
return b.nonconcurrentDB.TruncateTable(table)
|
||||
}
|
||||
|
||||
// AddColumn implements the [dbx.Builder.AddColumn] interface method.
|
||||
func (b *dualDBBuilder) AddColumn(table, col, typ string) *dbx.Query {
|
||||
return b.nonconcurrentDB.AddColumn(table, col, typ)
|
||||
}
|
||||
|
||||
// DropColumn implements the [dbx.Builder.DropColumn] interface method.
|
||||
func (b *dualDBBuilder) DropColumn(table, col string) *dbx.Query {
|
||||
return b.nonconcurrentDB.DropColumn(table, col)
|
||||
}
|
||||
|
||||
// RenameColumn implements the [dbx.Builder.RenameColumn] interface method.
|
||||
func (b *dualDBBuilder) RenameColumn(table, oldName, newName string) *dbx.Query {
|
||||
return b.nonconcurrentDB.RenameColumn(table, oldName, newName)
|
||||
}
|
||||
|
||||
// AlterColumn implements the [dbx.Builder.AlterColumn] interface method.
|
||||
func (b *dualDBBuilder) AlterColumn(table, col, typ string) *dbx.Query {
|
||||
return b.nonconcurrentDB.AlterColumn(table, col, typ)
|
||||
}
|
||||
|
||||
// AddPrimaryKey implements the [dbx.Builder.AddPrimaryKey] interface method.
|
||||
func (b *dualDBBuilder) AddPrimaryKey(table, name string, cols ...string) *dbx.Query {
|
||||
return b.nonconcurrentDB.AddPrimaryKey(table, name, cols...)
|
||||
}
|
||||
|
||||
// DropPrimaryKey implements the [dbx.Builder.DropPrimaryKey] interface method.
|
||||
func (b *dualDBBuilder) DropPrimaryKey(table, name string) *dbx.Query {
|
||||
return b.nonconcurrentDB.DropPrimaryKey(table, name)
|
||||
}
|
||||
|
||||
// AddForeignKey implements the [dbx.Builder.AddForeignKey] interface method.
|
||||
func (b *dualDBBuilder) AddForeignKey(table, name string, cols, refCols []string, refTable string, options ...string) *dbx.Query {
|
||||
return b.nonconcurrentDB.AddForeignKey(table, name, cols, refCols, refTable, options...)
|
||||
}
|
||||
|
||||
// DropForeignKey implements the [dbx.Builder.DropForeignKey] interface method.
|
||||
func (b *dualDBBuilder) DropForeignKey(table, name string) *dbx.Query {
|
||||
return b.nonconcurrentDB.DropForeignKey(table, name)
|
||||
}
|
||||
|
||||
// CreateIndex implements the [dbx.Builder.CreateIndex] interface method.
|
||||
func (b *dualDBBuilder) CreateIndex(table, name string, cols ...string) *dbx.Query {
|
||||
return b.nonconcurrentDB.CreateIndex(table, name, cols...)
|
||||
}
|
||||
|
||||
// CreateUniqueIndex implements the [dbx.Builder.CreateUniqueIndex] interface method.
|
||||
func (b *dualDBBuilder) CreateUniqueIndex(table, name string, cols ...string) *dbx.Query {
|
||||
return b.nonconcurrentDB.CreateUniqueIndex(table, name, cols...)
|
||||
}
|
||||
|
||||
// DropIndex implements the [dbx.Builder.DropIndex] interface method.
|
||||
func (b *dualDBBuilder) DropIndex(table, name string) *dbx.Query {
|
||||
return b.nonconcurrentDB.DropIndex(table, name)
|
||||
}
|
||||
|
||||
// NewQuery implements the [dbx.Builder.NewQuery] interface method by
|
||||
// routing the SELECT queries to the concurrent builder instance.
|
||||
func (b *dualDBBuilder) NewQuery(str string) *dbx.Query {
|
||||
// note: technically INSERT/UPDATE/DELETE could also have CTE but since
|
||||
// it is rare for now this scase is ignored to avoid unnecessary complicating the checks
|
||||
trimmed := trimLeftSpaces(str)
|
||||
if hasPrefixFold(trimmed, "SELECT") || hasPrefixFold(trimmed, "WITH") {
|
||||
return b.concurrentDB.NewQuery(str)
|
||||
}
|
||||
|
||||
return b.nonconcurrentDB.NewQuery(str)
|
||||
}
|
||||
|
||||
var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1}
|
||||
|
||||
// note: similar to strings.Space() but without the right trim because it is not needed in our case
|
||||
func trimLeftSpaces(str string) string {
|
||||
start := 0
|
||||
for ; start < len(str); start++ {
|
||||
c := str[start]
|
||||
if c >= utf8.RuneSelf {
|
||||
return strings.TrimLeftFunc(str[start:], unicode.IsSpace)
|
||||
}
|
||||
if asciiSpace[c] == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return str[start:]
|
||||
}
|
||||
|
||||
// note: the prefix is expected to be ASCII
|
||||
func hasPrefixFold(str, prefix string) bool {
|
||||
if len(str) < len(prefix) {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.EqualFold(str[:len(prefix)], prefix)
|
||||
}
|
||||
+12
-14
@@ -11,7 +11,7 @@ import (
|
||||
func (app *BaseApp) TableColumns(tableName string) ([]string, error) {
|
||||
columns := []string{}
|
||||
|
||||
err := app.DB().NewQuery("SELECT name FROM PRAGMA_TABLE_INFO({:tableName})").
|
||||
err := app.ConcurrentDB().NewQuery("SELECT name FROM PRAGMA_TABLE_INFO({:tableName})").
|
||||
Bind(dbx.Params{"tableName": tableName}).
|
||||
Column(&columns)
|
||||
|
||||
@@ -34,7 +34,7 @@ type TableInfoRow struct {
|
||||
func (app *BaseApp) TableInfo(tableName string) ([]*TableInfoRow, error) {
|
||||
info := []*TableInfoRow{}
|
||||
|
||||
err := app.DB().NewQuery("SELECT * FROM PRAGMA_TABLE_INFO({:tableName})").
|
||||
err := app.ConcurrentDB().NewQuery("SELECT * FROM PRAGMA_TABLE_INFO({:tableName})").
|
||||
Bind(dbx.Params{"tableName": tableName}).
|
||||
All(&info)
|
||||
if err != nil {
|
||||
@@ -59,7 +59,7 @@ func (app *BaseApp) TableIndexes(tableName string) (map[string]string, error) {
|
||||
Sql string
|
||||
}{}
|
||||
|
||||
err := app.DB().Select("name", "sql").
|
||||
err := app.ConcurrentDB().Select("name", "sql").
|
||||
From("sqlite_master").
|
||||
AndWhere(dbx.NewExp("sql is not null")).
|
||||
AndWhere(dbx.HashExp{
|
||||
@@ -87,7 +87,7 @@ func (app *BaseApp) TableIndexes(tableName string) (map[string]string, error) {
|
||||
// NB! Be aware that this method is vulnerable to SQL injection and the
|
||||
// "tableName" argument must come only from trusted input!
|
||||
func (app *BaseApp) DeleteTable(tableName string) error {
|
||||
_, err := app.DB().NewQuery(fmt.Sprintf(
|
||||
_, err := app.NonconcurrentDB().NewQuery(fmt.Sprintf(
|
||||
"DROP TABLE IF EXISTS {{%s}}",
|
||||
tableName,
|
||||
)).Execute()
|
||||
@@ -96,15 +96,15 @@ func (app *BaseApp) DeleteTable(tableName string) error {
|
||||
}
|
||||
|
||||
// HasTable checks if a table (or view) with the provided name exists (case insensitive).
|
||||
// in the current app.DB() instance.
|
||||
// in the data.db.
|
||||
func (app *BaseApp) HasTable(tableName string) bool {
|
||||
return app.hasTable(app.DB(), tableName)
|
||||
return app.hasTable(app.ConcurrentDB(), tableName)
|
||||
}
|
||||
|
||||
// AuxHasTable checks if a table (or view) with the provided name exists (case insensitive)
|
||||
// in the current app.AuxDB() instance.
|
||||
// in the auixiliary.db.
|
||||
func (app *BaseApp) AuxHasTable(tableName string) bool {
|
||||
return app.hasTable(app.AuxDB(), tableName)
|
||||
return app.hasTable(app.AuxConcurrentDB(), tableName)
|
||||
}
|
||||
|
||||
func (app *BaseApp) hasTable(db dbx.Builder, tableName string) bool {
|
||||
@@ -120,16 +120,14 @@ func (app *BaseApp) hasTable(db dbx.Builder, tableName string) bool {
|
||||
return err == nil && exists > 0
|
||||
}
|
||||
|
||||
// Vacuum executes VACUUM on the current app.DB() instance
|
||||
// in order to reclaim unused data db disk space.
|
||||
// Vacuum executes VACUUM on the data.db in order to reclaim unused data db disk space.
|
||||
func (app *BaseApp) Vacuum() error {
|
||||
return app.vacuum(app.DB())
|
||||
return app.vacuum(app.NonconcurrentDB())
|
||||
}
|
||||
|
||||
// AuxVacuum executes VACUUM on the current app.AuxDB() instance
|
||||
// in order to reclaim unused auxiliary db disk space.
|
||||
// AuxVacuum executes VACUUM on the auxiliary.db in order to reclaim unused auxiliary db disk space.
|
||||
func (app *BaseApp) AuxVacuum() error {
|
||||
return app.vacuum(app.AuxDB())
|
||||
return app.vacuum(app.AuxNonconcurrentDB())
|
||||
}
|
||||
|
||||
func (app *BaseApp) vacuum(db dbx.Builder) error {
|
||||
|
||||
@@ -202,10 +202,10 @@ func TestVacuum(t *testing.T) {
|
||||
defer app.Cleanup()
|
||||
|
||||
calledQueries := []string{}
|
||||
app.DB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
app.NonconcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
calledQueries = append(calledQueries, sql)
|
||||
}
|
||||
app.DB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
||||
app.NonconcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
||||
calledQueries = append(calledQueries, sql)
|
||||
}
|
||||
|
||||
@@ -229,10 +229,10 @@ func TestAuxVacuum(t *testing.T) {
|
||||
defer app.Cleanup()
|
||||
|
||||
calledQueries := []string{}
|
||||
app.AuxDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
app.AuxNonconcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
calledQueries = append(calledQueries, sql)
|
||||
}
|
||||
app.AuxDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
||||
app.AuxNonconcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
||||
calledQueries = append(calledQueries, sql)
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -38,12 +38,12 @@ func TestModelQuery(t *testing.T) {
|
||||
modelsQuery := app.ModelQuery(&core.Collection{})
|
||||
logsModelQuery := app.AuxModelQuery(&core.Collection{})
|
||||
|
||||
if app.DB() == modelsQuery.Info().Builder {
|
||||
t.Fatalf("ModelQuery() is not using app.DB()")
|
||||
if app.ConcurrentDB() == modelsQuery.Info().Builder {
|
||||
t.Fatalf("ModelQuery() is not using app.ConcurrentDB()")
|
||||
}
|
||||
|
||||
if app.AuxDB() == logsModelQuery.Info().Builder {
|
||||
t.Fatalf("AuxModelQuery() is not using app.AuxDB()")
|
||||
if app.AuxConcurrentDB() == logsModelQuery.Info().Builder {
|
||||
t.Fatalf("AuxModelQuery() is not using app.AuxConcurrentDB()")
|
||||
}
|
||||
|
||||
expectedSQL := "SELECT {{_collections}}.* FROM `_collections`"
|
||||
|
||||
+12
-5
@@ -60,7 +60,7 @@ func (app *BaseApp) createTxApp(tx *dbx.Tx, isForAuxDB bool) *BaseApp {
|
||||
clone.nonconcurrentDB = tx
|
||||
}
|
||||
|
||||
clone.txInfo = &txAppInfo{
|
||||
clone.txInfo = &TxAppInfo{
|
||||
parent: app,
|
||||
isForAuxDB: isForAuxDB,
|
||||
}
|
||||
@@ -68,22 +68,29 @@ func (app *BaseApp) createTxApp(tx *dbx.Tx, isForAuxDB bool) *BaseApp {
|
||||
return &clone
|
||||
}
|
||||
|
||||
type txAppInfo struct {
|
||||
// TxAppInfo represents an active transaction context associated to an existing app instance.
|
||||
type TxAppInfo struct {
|
||||
parent *BaseApp
|
||||
afterFuncs []func(txErr error) error
|
||||
mu sync.Mutex
|
||||
isForAuxDB bool
|
||||
}
|
||||
|
||||
func (a *txAppInfo) onAfterFunc(fn func(txErr error) error) {
|
||||
// OnComplete registers the provided callback that will be invoked
|
||||
// once the related transaction ends (either completes successfully or rollbacked with an error).
|
||||
//
|
||||
// The callback receives the transaction error (if any) as its argument.
|
||||
// Any additional errors returned by the OnComplete callbacks will be
|
||||
// joined together with txErr when returning the final transaction result.
|
||||
func (a *TxAppInfo) OnComplete(fn func(txErr error) error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
a.afterFuncs = append(a.afterFuncs, fn)
|
||||
}
|
||||
|
||||
// note: can be called only once because txAppInfo is cleared
|
||||
func (a *txAppInfo) runAfterFuncs(txErr error) error {
|
||||
// note: can be called only once because TxAppInfo is cleared
|
||||
func (a *TxAppInfo) runAfterFuncs(txErr error) error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -104,6 +105,11 @@ type ServeEvent struct {
|
||||
Server *http.Server
|
||||
CertManager *autocert.Manager
|
||||
|
||||
// Listener allow specifying a custom network listener.
|
||||
//
|
||||
// Leave it nil to use the default net.Listen("tcp", e.Server.Addr).
|
||||
Listener net.Listener
|
||||
|
||||
// InstallerFunc is the "installer" function that is called after
|
||||
// successful server tcp bind but only if there is no explicit
|
||||
// superuser record created yet.
|
||||
|
||||
@@ -155,7 +155,7 @@ func TestFindFirstExternalAuthByExpr(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d_%v", i, s.expr.Build(app.DB().(*dbx.DB), dbx.Params{})), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%d_%v", i, s.expr.Build(app.ConcurrentDB().(*dbx.DB), dbx.Params{})), func(t *testing.T) {
|
||||
result, err := app.FindFirstExternalAuthByExpr(s.expr)
|
||||
|
||||
hasErr := err != nil
|
||||
|
||||
+1
-3
@@ -10,7 +10,6 @@ import (
|
||||
"strings"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core/validators"
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
@@ -357,8 +356,7 @@ func (f *FileField) Intercept(
|
||||
return nil
|
||||
case InterceptorActionAfterCreateError, InterceptorActionAfterUpdateError:
|
||||
// when in transaction we assume that the error was handled by afterRecordExecuteFailure
|
||||
_, insideTransaction := app.DB().(*dbx.Tx)
|
||||
if insideTransaction {
|
||||
if app.IsTransactional() {
|
||||
return actionFunc()
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ func init() {
|
||||
|
||||
const FieldTypeJSON = "json"
|
||||
|
||||
const DefaultJSONFieldMaxSize int64 = 5 << 20
|
||||
const DefaultJSONFieldMaxSize int64 = 1 << 20
|
||||
|
||||
var (
|
||||
_ Field = (*JSONField)(nil)
|
||||
@@ -53,7 +53,7 @@ type JSONField struct {
|
||||
|
||||
// MaxSize specifies the maximum size of the allowed field value (in bytes and up to 2^53-1).
|
||||
//
|
||||
// If zero, a default limit of 5MB is applied.
|
||||
// If zero, a default limit of 1MB is applied.
|
||||
MaxSize int64 `form:"maxSize" json:"maxSize"`
|
||||
|
||||
// Required will require the field value to be non-empty JSON value
|
||||
|
||||
@@ -146,7 +146,7 @@ func TestJSONFieldValidateValue(t *testing.T) {
|
||||
&core.JSONField{Name: "test"},
|
||||
func() *core.Record {
|
||||
record := core.NewRecord(collection)
|
||||
record.SetRaw("test", types.JSONRaw(`"`+strings.Repeat("a", (5<<20))+`"`))
|
||||
record.SetRaw("test", types.JSONRaw(`"`+strings.Repeat("a", (1<<20))+`"`))
|
||||
return record
|
||||
},
|
||||
true,
|
||||
|
||||
@@ -219,7 +219,7 @@ func (f *RelationField) ValidateValue(ctx context.Context, app App, record *Reco
|
||||
}
|
||||
|
||||
var total int
|
||||
_ = app.DB().
|
||||
_ = app.ConcurrentDB().
|
||||
Select("count(*)").
|
||||
From(relCollection.Name).
|
||||
AndWhere(dbx.In("id", list.ToInterfaceSlice(ids)...)).
|
||||
|
||||
+1
-1
@@ -194,7 +194,7 @@ func (f *TextField) ValidateValue(ctx context.Context, app App, record *Record)
|
||||
// (@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 int
|
||||
err := app.DB().
|
||||
err := app.ConcurrentDB().
|
||||
Select("(1)").
|
||||
From(record.TableName()).
|
||||
Where(dbx.NewExp("id = {:id} COLLATE NOCASE", dbx.Params{"id": newVal})).
|
||||
|
||||
@@ -55,10 +55,10 @@ func TestBaseAppLoggerLevelDevPrint(t *testing.T) {
|
||||
}
|
||||
|
||||
// silence query logs
|
||||
app.DB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {}
|
||||
app.DB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {}
|
||||
app.NonconcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {}
|
||||
app.NonconcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {}
|
||||
app.concurrentDB.(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {}
|
||||
app.concurrentDB.(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {}
|
||||
app.nonconcurrentDB.(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {}
|
||||
app.nonconcurrentDB.(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {}
|
||||
|
||||
app.Settings().Logs.MinLevel = testLogLevel
|
||||
if err := app.Save(app.Settings()); err != nil {
|
||||
|
||||
@@ -173,7 +173,7 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"demo4",
|
||||
"self_rel_many.id ?> true", // should have join
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] WHERE [[demo4_self_rel_many.id]] > 1",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo4.self_rel_many]]), json_type([[demo4.self_rel_many]])='array', FALSE) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] WHERE [[demo4_self_rel_many.id]] > 1",
|
||||
},
|
||||
{
|
||||
"nested single rel (self rel)",
|
||||
@@ -201,63 +201,63 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"demo4",
|
||||
"self_rel_many.self_rel_one ?> true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] WHERE [[demo4_self_rel_many.self_rel_one]] > 1",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo4.self_rel_many]]), json_type([[demo4.self_rel_many]])='array', FALSE) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] WHERE [[demo4_self_rel_many.self_rel_one]] > 1",
|
||||
},
|
||||
{
|
||||
"nested incomplete relations (multi-match operator)",
|
||||
"demo4",
|
||||
"self_rel_many.self_rel_one > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] WHERE ((([[demo4_self_rel_many.self_rel_one]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many.self_rel_one]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_many]]) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > 1)))))",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo4.self_rel_many]]), json_type([[demo4.self_rel_many]])='array', FALSE) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] WHERE ((([[demo4_self_rel_many.self_rel_one]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many.self_rel_one]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo4.self_rel_many]]), json_type([[__mm_demo4.self_rel_many]])='array', FALSE) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > 1)))))",
|
||||
},
|
||||
{
|
||||
"nested complete relations (opt/any operator)",
|
||||
"demo4",
|
||||
"self_rel_many.self_rel_one.title ?> true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many.self_rel_one]] WHERE [[demo4_self_rel_many_self_rel_one.title]] > 1",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo4.self_rel_many]]), json_type([[demo4.self_rel_many]])='array', FALSE) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many.self_rel_one]] WHERE [[demo4_self_rel_many_self_rel_one.title]] > 1",
|
||||
},
|
||||
{
|
||||
"nested complete relations (multi-match operator)",
|
||||
"demo4",
|
||||
"self_rel_many.self_rel_one.title > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many.self_rel_one]] WHERE ((([[demo4_self_rel_many_self_rel_one.title]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many_self_rel_one.title]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_many]]) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one` ON [[__mm_demo4_self_rel_many_self_rel_one.id]] = [[__mm_demo4_self_rel_many.self_rel_one]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > 1)))))",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo4.self_rel_many]]), json_type([[demo4.self_rel_many]])='array', FALSE) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many.self_rel_one]] WHERE ((([[demo4_self_rel_many_self_rel_one.title]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many_self_rel_one.title]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo4.self_rel_many]]), json_type([[__mm_demo4.self_rel_many]])='array', FALSE) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one` ON [[__mm_demo4_self_rel_many_self_rel_one.id]] = [[__mm_demo4_self_rel_many.self_rel_one]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > 1)))))",
|
||||
},
|
||||
{
|
||||
"repeated nested relations (opt/any operator)",
|
||||
"demo4",
|
||||
"self_rel_many.self_rel_one.self_rel_many.self_rel_one.title ?> true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many.self_rel_one]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_many_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_many_self_rel_one.self_rel_many]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many` ON [[demo4_self_rel_many_self_rel_one_self_rel_many.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] WHERE [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.title]] > 1",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo4.self_rel_many]]), json_type([[demo4.self_rel_many]])='array', FALSE) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many.self_rel_one]] LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo4_self_rel_many_self_rel_one.self_rel_many]]), json_type([[demo4_self_rel_many_self_rel_one.self_rel_many]])='array', FALSE) THEN [[demo4_self_rel_many_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_many_self_rel_one.self_rel_many]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many` ON [[demo4_self_rel_many_self_rel_one_self_rel_many.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] WHERE [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.title]] > 1",
|
||||
},
|
||||
{
|
||||
"repeated nested relations (multi-match operator)",
|
||||
"demo4",
|
||||
"self_rel_many.self_rel_one.self_rel_many.self_rel_one.title > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many.self_rel_one]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_many_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_many_self_rel_one.self_rel_many]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many` ON [[demo4_self_rel_many_self_rel_one_self_rel_many.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] WHERE ((([[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.title]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.title]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_many]]) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one` ON [[__mm_demo4_self_rel_many_self_rel_one.id]] = [[__mm_demo4_self_rel_many.self_rel_one]] LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4_self_rel_many_self_rel_one.self_rel_many]]) THEN [[__mm_demo4_self_rel_many_self_rel_one.self_rel_many]] ELSE json_array([[__mm_demo4_self_rel_many_self_rel_one.self_rel_many]]) END) `__mm_demo4_self_rel_many_self_rel_one_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one_self_rel_many` ON [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many.id]] = [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many_je.value]] LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one` ON [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.id]] = [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > 1)))))",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo4.self_rel_many]]), json_type([[demo4.self_rel_many]])='array', FALSE) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many.self_rel_one]] LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo4_self_rel_many_self_rel_one.self_rel_many]]), json_type([[demo4_self_rel_many_self_rel_one.self_rel_many]])='array', FALSE) THEN [[demo4_self_rel_many_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_many_self_rel_one.self_rel_many]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many` ON [[demo4_self_rel_many_self_rel_one_self_rel_many.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] WHERE ((([[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.title]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.title]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo4.self_rel_many]]), json_type([[__mm_demo4.self_rel_many]])='array', FALSE) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one` ON [[__mm_demo4_self_rel_many_self_rel_one.id]] = [[__mm_demo4_self_rel_many.self_rel_one]] LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo4_self_rel_many_self_rel_one.self_rel_many]]), json_type([[__mm_demo4_self_rel_many_self_rel_one.self_rel_many]])='array', FALSE) THEN [[__mm_demo4_self_rel_many_self_rel_one.self_rel_many]] ELSE json_array([[__mm_demo4_self_rel_many_self_rel_one.self_rel_many]]) END) `__mm_demo4_self_rel_many_self_rel_one_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one_self_rel_many` ON [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many.id]] = [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many_je.value]] LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one` ON [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.id]] = [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > 1)))))",
|
||||
},
|
||||
{
|
||||
"multiple relations (opt/any operators)",
|
||||
"demo4",
|
||||
"self_rel_many.title ?= 'test' || self_rel_one.json_object.a ?> true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] WHERE ([[demo4_self_rel_many.title]] = {:TEST} OR (CASE WHEN json_valid([[demo4_self_rel_one.json_object]]) THEN JSON_EXTRACT([[demo4_self_rel_one.json_object]], '$.a') ELSE JSON_EXTRACT(json_object('pb', [[demo4_self_rel_one.json_object]]), '$.pb.a') END) > 1)",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo4.self_rel_many]]), json_type([[demo4.self_rel_many]])='array', FALSE) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] WHERE ([[demo4_self_rel_many.title]] = {:TEST} OR (CASE WHEN json_valid([[demo4_self_rel_one.json_object]]) THEN JSON_EXTRACT([[demo4_self_rel_one.json_object]], '$.a') ELSE JSON_EXTRACT(json_object('pb', [[demo4_self_rel_one.json_object]]), '$.pb.a') END) > 1)",
|
||||
},
|
||||
{
|
||||
"multiple relations (multi-match operators)",
|
||||
"demo4",
|
||||
"self_rel_many.title = 'test' || self_rel_one.json_object.a > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] WHERE ((([[demo4_self_rel_many.title]] = {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many.title]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_many]]) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] = {:TEST})))) OR (CASE WHEN json_valid([[demo4_self_rel_one.json_object]]) THEN JSON_EXTRACT([[demo4_self_rel_one.json_object]], '$.a') ELSE JSON_EXTRACT(json_object('pb', [[demo4_self_rel_one.json_object]]), '$.pb.a') END) > 1)",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo4.self_rel_many]]), json_type([[demo4.self_rel_many]])='array', FALSE) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] WHERE ((([[demo4_self_rel_many.title]] = {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many.title]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo4.self_rel_many]]), json_type([[__mm_demo4.self_rel_many]])='array', FALSE) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] = {:TEST})))) OR (CASE WHEN json_valid([[demo4_self_rel_one.json_object]]) THEN JSON_EXTRACT([[demo4_self_rel_one.json_object]], '$.a') ELSE JSON_EXTRACT(json_object('pb', [[demo4_self_rel_one.json_object]]), '$.pb.a') END) > 1)",
|
||||
},
|
||||
{
|
||||
"back relations via single relation field (without unique index)",
|
||||
"demo3",
|
||||
"demo4_via_rel_one_cascade.id = true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo3`.* FROM `demo3` LEFT JOIN `demo4` `demo3_demo4_via_rel_one_cascade` ON [[demo3.id]] IN (SELECT [[demo3_demo4_via_rel_one_cascade_je.value]] FROM json_each(CASE WHEN json_valid([[demo3_demo4_via_rel_one_cascade.rel_one_cascade]]) THEN [[demo3_demo4_via_rel_one_cascade.rel_one_cascade]] ELSE json_array([[demo3_demo4_via_rel_one_cascade.rel_one_cascade]]) END) {{demo3_demo4_via_rel_one_cascade_je}}) WHERE ((([[demo3_demo4_via_rel_one_cascade.id]] = 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo3_demo4_via_rel_one_cascade.id]] as [[multiMatchValue]] FROM `demo3` `__mm_demo3` LEFT JOIN `demo4` `__mm_demo3_demo4_via_rel_one_cascade` ON [[__mm_demo3.id]] IN (SELECT [[__mm_demo3_demo4_via_rel_one_cascade_je.value]] FROM json_each(CASE WHEN json_valid([[__mm_demo3_demo4_via_rel_one_cascade.rel_one_cascade]]) THEN [[__mm_demo3_demo4_via_rel_one_cascade.rel_one_cascade]] ELSE json_array([[__mm_demo3_demo4_via_rel_one_cascade.rel_one_cascade]]) END) {{__mm_demo3_demo4_via_rel_one_cascade_je}}) WHERE `__mm_demo3`.`id` = `demo3`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] = 1)))))",
|
||||
"SELECT DISTINCT `demo3`.* FROM `demo3` LEFT JOIN `demo4` `demo3_demo4_via_rel_one_cascade` ON [[demo3.id]] IN (SELECT [[demo3_demo4_via_rel_one_cascade_je.value]] FROM json_each(CASE WHEN iif(json_valid([[demo3_demo4_via_rel_one_cascade.rel_one_cascade]]), json_type([[demo3_demo4_via_rel_one_cascade.rel_one_cascade]])='array', FALSE) THEN [[demo3_demo4_via_rel_one_cascade.rel_one_cascade]] ELSE json_array([[demo3_demo4_via_rel_one_cascade.rel_one_cascade]]) END) {{demo3_demo4_via_rel_one_cascade_je}}) WHERE ((([[demo3_demo4_via_rel_one_cascade.id]] = 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo3_demo4_via_rel_one_cascade.id]] as [[multiMatchValue]] FROM `demo3` `__mm_demo3` LEFT JOIN `demo4` `__mm_demo3_demo4_via_rel_one_cascade` ON [[__mm_demo3.id]] IN (SELECT [[__mm_demo3_demo4_via_rel_one_cascade_je.value]] FROM json_each(CASE WHEN iif(json_valid([[__mm_demo3_demo4_via_rel_one_cascade.rel_one_cascade]]), json_type([[__mm_demo3_demo4_via_rel_one_cascade.rel_one_cascade]])='array', FALSE) THEN [[__mm_demo3_demo4_via_rel_one_cascade.rel_one_cascade]] ELSE json_array([[__mm_demo3_demo4_via_rel_one_cascade.rel_one_cascade]]) END) {{__mm_demo3_demo4_via_rel_one_cascade_je}}) WHERE `__mm_demo3`.`id` = `demo3`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] = 1)))))",
|
||||
},
|
||||
{
|
||||
"back relations via single relation field (with unique index)",
|
||||
@@ -271,28 +271,28 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"demo3",
|
||||
"demo4_via_rel_many_cascade.id ?= true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo3`.* FROM `demo3` LEFT JOIN `demo4` `demo3_demo4_via_rel_many_cascade` ON [[demo3.id]] IN (SELECT [[demo3_demo4_via_rel_many_cascade_je.value]] FROM json_each(CASE WHEN json_valid([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) THEN [[demo3_demo4_via_rel_many_cascade.rel_many_cascade]] ELSE json_array([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) END) {{demo3_demo4_via_rel_many_cascade_je}}) WHERE [[demo3_demo4_via_rel_many_cascade.id]] = 1",
|
||||
"SELECT DISTINCT `demo3`.* FROM `demo3` LEFT JOIN `demo4` `demo3_demo4_via_rel_many_cascade` ON [[demo3.id]] IN (SELECT [[demo3_demo4_via_rel_many_cascade_je.value]] FROM json_each(CASE WHEN iif(json_valid([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]), json_type([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]])='array', FALSE) THEN [[demo3_demo4_via_rel_many_cascade.rel_many_cascade]] ELSE json_array([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) END) {{demo3_demo4_via_rel_many_cascade_je}}) WHERE [[demo3_demo4_via_rel_many_cascade.id]] = 1",
|
||||
},
|
||||
{
|
||||
"back relations via multiple relation field (multi-match operators)",
|
||||
"demo3",
|
||||
"demo4_via_rel_many_cascade.id = true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo3`.* FROM `demo3` LEFT JOIN `demo4` `demo3_demo4_via_rel_many_cascade` ON [[demo3.id]] IN (SELECT [[demo3_demo4_via_rel_many_cascade_je.value]] FROM json_each(CASE WHEN json_valid([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) THEN [[demo3_demo4_via_rel_many_cascade.rel_many_cascade]] ELSE json_array([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) END) {{demo3_demo4_via_rel_many_cascade_je}}) WHERE ((([[demo3_demo4_via_rel_many_cascade.id]] = 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo3_demo4_via_rel_many_cascade.id]] as [[multiMatchValue]] FROM `demo3` `__mm_demo3` LEFT JOIN `demo4` `__mm_demo3_demo4_via_rel_many_cascade` ON [[__mm_demo3.id]] IN (SELECT [[__mm_demo3_demo4_via_rel_many_cascade_je.value]] FROM json_each(CASE WHEN json_valid([[__mm_demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) THEN [[__mm_demo3_demo4_via_rel_many_cascade.rel_many_cascade]] ELSE json_array([[__mm_demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) END) {{__mm_demo3_demo4_via_rel_many_cascade_je}}) WHERE `__mm_demo3`.`id` = `demo3`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] = 1)))))",
|
||||
"SELECT DISTINCT `demo3`.* FROM `demo3` LEFT JOIN `demo4` `demo3_demo4_via_rel_many_cascade` ON [[demo3.id]] IN (SELECT [[demo3_demo4_via_rel_many_cascade_je.value]] FROM json_each(CASE WHEN iif(json_valid([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]), json_type([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]])='array', FALSE) THEN [[demo3_demo4_via_rel_many_cascade.rel_many_cascade]] ELSE json_array([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) END) {{demo3_demo4_via_rel_many_cascade_je}}) WHERE ((([[demo3_demo4_via_rel_many_cascade.id]] = 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo3_demo4_via_rel_many_cascade.id]] as [[multiMatchValue]] FROM `demo3` `__mm_demo3` LEFT JOIN `demo4` `__mm_demo3_demo4_via_rel_many_cascade` ON [[__mm_demo3.id]] IN (SELECT [[__mm_demo3_demo4_via_rel_many_cascade_je.value]] FROM json_each(CASE WHEN iif(json_valid([[__mm_demo3_demo4_via_rel_many_cascade.rel_many_cascade]]), json_type([[__mm_demo3_demo4_via_rel_many_cascade.rel_many_cascade]])='array', FALSE) THEN [[__mm_demo3_demo4_via_rel_many_cascade.rel_many_cascade]] ELSE json_array([[__mm_demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) END) {{__mm_demo3_demo4_via_rel_many_cascade_je}}) WHERE `__mm_demo3`.`id` = `demo3`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] = 1)))))",
|
||||
},
|
||||
{
|
||||
"back relations via unique multiple relation field (should be the same as multi-match)",
|
||||
"demo3",
|
||||
"demo4_via_rel_many_unique.id = true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo3`.* FROM `demo3` LEFT JOIN `demo4` `demo3_demo4_via_rel_many_unique` ON [[demo3.id]] IN (SELECT [[demo3_demo4_via_rel_many_unique_je.value]] FROM json_each(CASE WHEN json_valid([[demo3_demo4_via_rel_many_unique.rel_many_unique]]) THEN [[demo3_demo4_via_rel_many_unique.rel_many_unique]] ELSE json_array([[demo3_demo4_via_rel_many_unique.rel_many_unique]]) END) {{demo3_demo4_via_rel_many_unique_je}}) WHERE ((([[demo3_demo4_via_rel_many_unique.id]] = 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo3_demo4_via_rel_many_unique.id]] as [[multiMatchValue]] FROM `demo3` `__mm_demo3` LEFT JOIN `demo4` `__mm_demo3_demo4_via_rel_many_unique` ON [[__mm_demo3.id]] IN (SELECT [[__mm_demo3_demo4_via_rel_many_unique_je.value]] FROM json_each(CASE WHEN json_valid([[__mm_demo3_demo4_via_rel_many_unique.rel_many_unique]]) THEN [[__mm_demo3_demo4_via_rel_many_unique.rel_many_unique]] ELSE json_array([[__mm_demo3_demo4_via_rel_many_unique.rel_many_unique]]) END) {{__mm_demo3_demo4_via_rel_many_unique_je}}) WHERE `__mm_demo3`.`id` = `demo3`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] = 1)))))",
|
||||
"SELECT DISTINCT `demo3`.* FROM `demo3` LEFT JOIN `demo4` `demo3_demo4_via_rel_many_unique` ON [[demo3.id]] IN (SELECT [[demo3_demo4_via_rel_many_unique_je.value]] FROM json_each(CASE WHEN iif(json_valid([[demo3_demo4_via_rel_many_unique.rel_many_unique]]), json_type([[demo3_demo4_via_rel_many_unique.rel_many_unique]])='array', FALSE) THEN [[demo3_demo4_via_rel_many_unique.rel_many_unique]] ELSE json_array([[demo3_demo4_via_rel_many_unique.rel_many_unique]]) END) {{demo3_demo4_via_rel_many_unique_je}}) WHERE ((([[demo3_demo4_via_rel_many_unique.id]] = 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo3_demo4_via_rel_many_unique.id]] as [[multiMatchValue]] FROM `demo3` `__mm_demo3` LEFT JOIN `demo4` `__mm_demo3_demo4_via_rel_many_unique` ON [[__mm_demo3.id]] IN (SELECT [[__mm_demo3_demo4_via_rel_many_unique_je.value]] FROM json_each(CASE WHEN iif(json_valid([[__mm_demo3_demo4_via_rel_many_unique.rel_many_unique]]), json_type([[__mm_demo3_demo4_via_rel_many_unique.rel_many_unique]])='array', FALSE) THEN [[__mm_demo3_demo4_via_rel_many_unique.rel_many_unique]] ELSE json_array([[__mm_demo3_demo4_via_rel_many_unique.rel_many_unique]]) END) {{__mm_demo3_demo4_via_rel_many_unique_je}}) WHERE `__mm_demo3`.`id` = `demo3`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] = 1)))))",
|
||||
},
|
||||
{
|
||||
"recursive back relations",
|
||||
"demo3",
|
||||
"demo4_via_rel_many_cascade.rel_one_cascade.demo4_via_rel_many_cascade.id ?= true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo3`.* FROM `demo3` LEFT JOIN `demo4` `demo3_demo4_via_rel_many_cascade` ON [[demo3.id]] IN (SELECT [[demo3_demo4_via_rel_many_cascade_je.value]] FROM json_each(CASE WHEN json_valid([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) THEN [[demo3_demo4_via_rel_many_cascade.rel_many_cascade]] ELSE json_array([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) END) {{demo3_demo4_via_rel_many_cascade_je}}) LEFT JOIN `demo3` `demo3_demo4_via_rel_many_cascade_rel_one_cascade` ON [[demo3_demo4_via_rel_many_cascade_rel_one_cascade.id]] = [[demo3_demo4_via_rel_many_cascade.rel_one_cascade]] LEFT JOIN `demo4` `demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade` ON [[demo3_demo4_via_rel_many_cascade_rel_one_cascade.id]] IN (SELECT [[demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade_je.value]] FROM json_each(CASE WHEN json_valid([[demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade.rel_many_cascade]]) THEN [[demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade.rel_many_cascade]] ELSE json_array([[demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade.rel_many_cascade]]) END) {{demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade_je}}) WHERE [[demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade.id]] = 1",
|
||||
"SELECT DISTINCT `demo3`.* FROM `demo3` LEFT JOIN `demo4` `demo3_demo4_via_rel_many_cascade` ON [[demo3.id]] IN (SELECT [[demo3_demo4_via_rel_many_cascade_je.value]] FROM json_each(CASE WHEN iif(json_valid([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]), json_type([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]])='array', FALSE) THEN [[demo3_demo4_via_rel_many_cascade.rel_many_cascade]] ELSE json_array([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) END) {{demo3_demo4_via_rel_many_cascade_je}}) LEFT JOIN `demo3` `demo3_demo4_via_rel_many_cascade_rel_one_cascade` ON [[demo3_demo4_via_rel_many_cascade_rel_one_cascade.id]] = [[demo3_demo4_via_rel_many_cascade.rel_one_cascade]] LEFT JOIN `demo4` `demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade` ON [[demo3_demo4_via_rel_many_cascade_rel_one_cascade.id]] IN (SELECT [[demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade_je.value]] FROM json_each(CASE WHEN iif(json_valid([[demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade.rel_many_cascade]]), json_type([[demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade.rel_many_cascade]])='array', FALSE) THEN [[demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade.rel_many_cascade]] ELSE json_array([[demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade.rel_many_cascade]]) END) {{demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade_je}}) WHERE [[demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade.id]] = 1",
|
||||
},
|
||||
{
|
||||
"@collection join (opt/any operators)",
|
||||
@@ -379,7 +379,7 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"rel_many.name:lower > true ||" +
|
||||
"created:lower > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN `users` `__data_users_rel_many` ON [[__data_users_rel_many.id]] IN ({:p0}, {:p1}) LEFT JOIN json_each(CASE WHEN json_valid([[demo1.rel_many]]) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END) `demo1_rel_many_je` LEFT JOIN `users` `demo1_rel_many` ON [[demo1_rel_many.id]] = [[demo1_rel_many_je.value]] WHERE (LOWER({:infoLowerrel_oneTEST}) > 1 OR LOWER({:infoLowerrel_manyTEST}) > 1 OR ((LOWER([[__data_users_rel_many.email]]) > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT LOWER([[__data_mm_users_rel_many.email]]) as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN `users` `__data_mm_users_rel_many` ON [[__data_mm_users_rel_many.id]] IN ({:p4}, {:p5}) WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > 1)))) OR LOWER([[demo1.text]]) > 1 OR LOWER([[demo1.bool]]) > 1 OR LOWER([[demo1.url]]) > 1 OR LOWER([[demo1.select_one]]) > 1 OR LOWER([[demo1.select_many]]) > 1 OR LOWER([[demo1.file_one]]) > 1 OR LOWER([[demo1.file_many]]) > 1 OR LOWER([[demo1.number]]) > 1 OR LOWER([[demo1.email]]) > 1 OR LOWER([[demo1.datetime]]) > 1 OR LOWER((CASE WHEN json_valid([[demo1.json]]) THEN JSON_EXTRACT([[demo1.json]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo1.json]]), '$.pb') END)) > 1 OR LOWER([[demo1.rel_one]]) > 1 OR LOWER([[demo1.rel_many]]) > 1 OR ((LOWER([[demo1_rel_many.name]]) > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT LOWER([[__mm_demo1_rel_many.name]]) as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.rel_many]]) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > 1)))) OR LOWER([[demo1.created]]) > 1)",
|
||||
"SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN `users` `__data_users_rel_many` ON [[__data_users_rel_many.id]] IN ({:p0}, {:p1}) LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo1.rel_many]]), json_type([[demo1.rel_many]])='array', FALSE) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END) `demo1_rel_many_je` LEFT JOIN `users` `demo1_rel_many` ON [[demo1_rel_many.id]] = [[demo1_rel_many_je.value]] WHERE (LOWER({:infoLowerrel_oneTEST}) > 1 OR LOWER({:infoLowerrel_manyTEST}) > 1 OR ((LOWER([[__data_users_rel_many.email]]) > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT LOWER([[__data_mm_users_rel_many.email]]) as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN `users` `__data_mm_users_rel_many` ON [[__data_mm_users_rel_many.id]] IN ({:p4}, {:p5}) WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > 1)))) OR LOWER([[demo1.text]]) > 1 OR LOWER([[demo1.bool]]) > 1 OR LOWER([[demo1.url]]) > 1 OR LOWER([[demo1.select_one]]) > 1 OR LOWER([[demo1.select_many]]) > 1 OR LOWER([[demo1.file_one]]) > 1 OR LOWER([[demo1.file_many]]) > 1 OR LOWER([[demo1.number]]) > 1 OR LOWER([[demo1.email]]) > 1 OR LOWER([[demo1.datetime]]) > 1 OR LOWER((CASE WHEN json_valid([[demo1.json]]) THEN JSON_EXTRACT([[demo1.json]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo1.json]]), '$.pb') END)) > 1 OR LOWER([[demo1.rel_one]]) > 1 OR LOWER([[demo1.rel_many]]) > 1 OR ((LOWER([[demo1_rel_many.name]]) > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT LOWER([[__mm_demo1_rel_many.name]]) as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo1.rel_many]]), json_type([[__mm_demo1.rel_many]])='array', FALSE) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > 1)))) OR LOWER([[demo1.created]]) > 1)",
|
||||
},
|
||||
{
|
||||
"isset modifier",
|
||||
@@ -440,7 +440,7 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"rel_many:each > true &&" +
|
||||
"rel_many:each ?< true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.select_one]]) THEN [[demo1.select_one]] ELSE json_array([[demo1.select_one]]) END) `demo1_select_one_je` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.select_many]]) THEN [[demo1.select_many]] ELSE json_array([[demo1.select_many]]) END) `demo1_select_many_je` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.file_one]]) THEN [[demo1.file_one]] ELSE json_array([[demo1.file_one]]) END) `demo1_file_one_je` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.file_many]]) THEN [[demo1.file_many]] ELSE json_array([[demo1.file_many]]) END) `demo1_file_many_je` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.rel_one]]) THEN [[demo1.rel_one]] ELSE json_array([[demo1.rel_one]]) END) `demo1_rel_one_je` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.rel_many]]) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END) `demo1_rel_many_je` WHERE ([[demo1_select_one_je.value]] > 1 AND [[demo1_select_one_je.value]] < 1 AND (([[demo1_select_many_je.value]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.select_many]]) THEN [[__mm_demo1.select_many]] ELSE json_array([[__mm_demo1.select_many]]) END) `__mm_demo1_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > 1)))) AND [[demo1_select_many_je.value]] < 1 AND [[demo1_file_one_je.value]] > 1 AND [[demo1_file_one_je.value]] < 1 AND (([[demo1_file_many_je.value]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_file_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.file_many]]) THEN [[__mm_demo1.file_many]] ELSE json_array([[__mm_demo1.file_many]]) END) `__mm_demo1_file_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > 1)))) AND [[demo1_file_many_je.value]] < 1 AND [[demo1_rel_one_je.value]] > 1 AND [[demo1_rel_one_je.value]] < 1 AND (([[demo1_rel_many_je.value]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_rel_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.rel_many]]) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > 1)))) AND [[demo1_rel_many_je.value]] < 1)",
|
||||
"SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo1.select_one]]), json_type([[demo1.select_one]])='array', FALSE) THEN [[demo1.select_one]] ELSE json_array([[demo1.select_one]]) END) `demo1_select_one_je` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo1.select_many]]), json_type([[demo1.select_many]])='array', FALSE) THEN [[demo1.select_many]] ELSE json_array([[demo1.select_many]]) END) `demo1_select_many_je` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo1.file_one]]), json_type([[demo1.file_one]])='array', FALSE) THEN [[demo1.file_one]] ELSE json_array([[demo1.file_one]]) END) `demo1_file_one_je` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo1.file_many]]), json_type([[demo1.file_many]])='array', FALSE) THEN [[demo1.file_many]] ELSE json_array([[demo1.file_many]]) END) `demo1_file_many_je` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo1.rel_one]]), json_type([[demo1.rel_one]])='array', FALSE) THEN [[demo1.rel_one]] ELSE json_array([[demo1.rel_one]]) END) `demo1_rel_one_je` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo1.rel_many]]), json_type([[demo1.rel_many]])='array', FALSE) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END) `demo1_rel_many_je` WHERE ([[demo1_select_one_je.value]] > 1 AND [[demo1_select_one_je.value]] < 1 AND (([[demo1_select_many_je.value]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo1.select_many]]), json_type([[__mm_demo1.select_many]])='array', FALSE) THEN [[__mm_demo1.select_many]] ELSE json_array([[__mm_demo1.select_many]]) END) `__mm_demo1_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > 1)))) AND [[demo1_select_many_je.value]] < 1 AND [[demo1_file_one_je.value]] > 1 AND [[demo1_file_one_je.value]] < 1 AND (([[demo1_file_many_je.value]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_file_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo1.file_many]]), json_type([[__mm_demo1.file_many]])='array', FALSE) THEN [[__mm_demo1.file_many]] ELSE json_array([[__mm_demo1.file_many]]) END) `__mm_demo1_file_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > 1)))) AND [[demo1_file_many_je.value]] < 1 AND [[demo1_rel_one_je.value]] > 1 AND [[demo1_rel_one_je.value]] < 1 AND (([[demo1_rel_many_je.value]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_rel_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo1.rel_many]]), json_type([[__mm_demo1.rel_many]])='array', FALSE) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > 1)))) AND [[demo1_rel_many_je.value]] < 1)",
|
||||
},
|
||||
{
|
||||
"arrayble:each vs arrayble:each",
|
||||
@@ -450,7 +450,7 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"select_many:each ?< select_one:each &&" +
|
||||
"select_many:each = @request.body.select_many:each",
|
||||
false,
|
||||
"SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.select_one]]) THEN [[demo1.select_one]] ELSE json_array([[demo1.select_one]]) END) `demo1_select_one_je` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.select_many]]) THEN [[demo1.select_many]] ELSE json_array([[demo1.select_many]]) END) `demo1_select_many_je` LEFT JOIN json_each({:dataEachTEST}) `__dataEach_select_many_je` WHERE (((COALESCE([[demo1_select_one_je.value]], '') IS NOT COALESCE([[demo1_select_many_je.value]], '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.select_many]]) THEN [[__mm_demo1.select_many]] ELSE json_array([[__mm_demo1.select_many]]) END) `__mm_demo1_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT (COALESCE([[demo1_select_one_je.value]], '') IS NOT COALESCE([[__smTEST.multiMatchValue]], ''))))) AND (([[demo1_select_many_je.value]] > [[demo1_select_one_je.value]]) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.select_many]]) THEN [[__mm_demo1.select_many]] ELSE json_array([[__mm_demo1.select_many]]) END) `__mm_demo1_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > [[demo1_select_one_je.value]])))) AND [[demo1_select_many_je.value]] < [[demo1_select_one_je.value]] AND (([[demo1_select_many_je.value]] = [[__dataEach_select_many_je.value]]) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.select_many]]) THEN [[__mm_demo1.select_many]] ELSE json_array([[__mm_demo1.select_many]]) END) `__mm_demo1_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mlTEST}} LEFT JOIN (SELECT [[__mm__dataEach_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each({:mmdataEachTEST}) `__mm__dataEach_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mrTEST}} WHERE NOT (COALESCE([[__mlTEST.multiMatchValue]], '') = COALESCE([[__mrTEST.multiMatchValue]], ''))))))",
|
||||
"SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo1.select_one]]), json_type([[demo1.select_one]])='array', FALSE) THEN [[demo1.select_one]] ELSE json_array([[demo1.select_one]]) END) `demo1_select_one_je` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo1.select_many]]), json_type([[demo1.select_many]])='array', FALSE) THEN [[demo1.select_many]] ELSE json_array([[demo1.select_many]]) END) `demo1_select_many_je` LEFT JOIN json_each({:dataEachTEST}) `__dataEach_select_many_je` WHERE (((COALESCE([[demo1_select_one_je.value]], '') IS NOT COALESCE([[demo1_select_many_je.value]], '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo1.select_many]]), json_type([[__mm_demo1.select_many]])='array', FALSE) THEN [[__mm_demo1.select_many]] ELSE json_array([[__mm_demo1.select_many]]) END) `__mm_demo1_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT (COALESCE([[demo1_select_one_je.value]], '') IS NOT COALESCE([[__smTEST.multiMatchValue]], ''))))) AND (([[demo1_select_many_je.value]] > [[demo1_select_one_je.value]]) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo1.select_many]]), json_type([[__mm_demo1.select_many]])='array', FALSE) THEN [[__mm_demo1.select_many]] ELSE json_array([[__mm_demo1.select_many]]) END) `__mm_demo1_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] > [[demo1_select_one_je.value]])))) AND [[demo1_select_many_je.value]] < [[demo1_select_one_je.value]] AND (([[demo1_select_many_je.value]] = [[__dataEach_select_many_je.value]]) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo1.select_many]]), json_type([[__mm_demo1.select_many]])='array', FALSE) THEN [[__mm_demo1.select_many]] ELSE json_array([[__mm_demo1.select_many]]) END) `__mm_demo1_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mlTEST}} LEFT JOIN (SELECT [[__mm__dataEach_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each({:mmdataEachTEST}) `__mm__dataEach_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mrTEST}} WHERE NOT (COALESCE([[__mlTEST.multiMatchValue]], '') = COALESCE([[__mrTEST.multiMatchValue]], ''))))))",
|
||||
},
|
||||
{
|
||||
"mixed multi-match vs multi-match",
|
||||
@@ -462,7 +462,7 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"@collection.demo2.active ?= rel_many.rel.active &&" +
|
||||
"rel_many.email > @request.body.rel_many.email",
|
||||
false,
|
||||
"SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.rel_many]]) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END) `demo1_rel_many_je` LEFT JOIN `users` `demo1_rel_many` ON [[demo1_rel_many.id]] = [[demo1_rel_many_je.value]] LEFT JOIN `demo2` `demo1_rel_many_rel` ON [[demo1_rel_many_rel.id]] = [[demo1_rel_many.rel]] LEFT JOIN `demo1` `demo1_rel_one` ON [[demo1_rel_one.id]] = [[demo1.rel_one]] LEFT JOIN `demo2` `__collection_demo2` LEFT JOIN `users` `__data_users_rel_many` ON [[__data_users_rel_many.id]] IN ({:p0}, {:p1}) WHERE (((COALESCE([[demo1_rel_many_rel.active]], '') IS NOT COALESCE([[demo1_rel_many.name]], '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_rel_many_rel.active]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.rel_many]]) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] LEFT JOIN `demo2` `__mm_demo1_rel_many_rel` ON [[__mm_demo1_rel_many_rel.id]] = [[__mm_demo1_rel_many.rel]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mlTEST}} LEFT JOIN (SELECT [[__mm_demo1_rel_many.name]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.rel_many]]) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mrTEST}} WHERE NOT (COALESCE([[__mlTEST.multiMatchValue]], '') IS NOT COALESCE([[__mrTEST.multiMatchValue]], ''))))) AND COALESCE([[demo1_rel_many_rel.active]], '') = COALESCE([[demo1_rel_many.name]], '') AND (([[demo1_rel_many_rel.title]] LIKE ('%' || [[demo1_rel_one.email]] || '%') ESCAPE '\\') AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_rel_many_rel.title]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.rel_many]]) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] LEFT JOIN `demo2` `__mm_demo1_rel_many_rel` ON [[__mm_demo1_rel_many_rel.id]] = [[__mm_demo1_rel_many.rel]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] LIKE ('%' || [[demo1_rel_one.email]] || '%') ESCAPE '\\')))) AND ((COALESCE([[__collection_demo2.active]], '') = COALESCE([[demo1_rel_many_rel.active]], '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm__collection_demo2.active]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN `demo2` `__mm__collection_demo2` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mlTEST}} LEFT JOIN (SELECT [[__mm_demo1_rel_many_rel.active]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.rel_many]]) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] LEFT JOIN `demo2` `__mm_demo1_rel_many_rel` ON [[__mm_demo1_rel_many_rel.id]] = [[__mm_demo1_rel_many.rel]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mrTEST}} WHERE NOT (COALESCE([[__mlTEST.multiMatchValue]], '') = COALESCE([[__mrTEST.multiMatchValue]], ''))))) AND COALESCE([[__collection_demo2.active]], '') = COALESCE([[demo1_rel_many_rel.active]], '') AND (((([[demo1_rel_many.email]] > [[__data_users_rel_many.email]]) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_rel_many.email]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.rel_many]]) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mlTEST}} LEFT JOIN (SELECT [[__data_mm_users_rel_many.email]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN `users` `__data_mm_users_rel_many` ON [[__data_mm_users_rel_many.id]] IN ({:p2}, {:p3}) WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mrTEST}} WHERE NOT ([[__mlTEST.multiMatchValue]] > [[__mrTEST.multiMatchValue]]))))) AND ([[demo1_rel_many.emailVisibility]] = TRUE)))",
|
||||
"SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo1.rel_many]]), json_type([[demo1.rel_many]])='array', FALSE) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END) `demo1_rel_many_je` LEFT JOIN `users` `demo1_rel_many` ON [[demo1_rel_many.id]] = [[demo1_rel_many_je.value]] LEFT JOIN `demo2` `demo1_rel_many_rel` ON [[demo1_rel_many_rel.id]] = [[demo1_rel_many.rel]] LEFT JOIN `demo1` `demo1_rel_one` ON [[demo1_rel_one.id]] = [[demo1.rel_one]] LEFT JOIN `demo2` `__collection_demo2` LEFT JOIN `users` `__data_users_rel_many` ON [[__data_users_rel_many.id]] IN ({:p0}, {:p1}) WHERE (((COALESCE([[demo1_rel_many_rel.active]], '') IS NOT COALESCE([[demo1_rel_many.name]], '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_rel_many_rel.active]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo1.rel_many]]), json_type([[__mm_demo1.rel_many]])='array', FALSE) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] LEFT JOIN `demo2` `__mm_demo1_rel_many_rel` ON [[__mm_demo1_rel_many_rel.id]] = [[__mm_demo1_rel_many.rel]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mlTEST}} LEFT JOIN (SELECT [[__mm_demo1_rel_many.name]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo1.rel_many]]), json_type([[__mm_demo1.rel_many]])='array', FALSE) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mrTEST}} WHERE NOT (COALESCE([[__mlTEST.multiMatchValue]], '') IS NOT COALESCE([[__mrTEST.multiMatchValue]], ''))))) AND COALESCE([[demo1_rel_many_rel.active]], '') = COALESCE([[demo1_rel_many.name]], '') AND (([[demo1_rel_many_rel.title]] LIKE ('%' || [[demo1_rel_one.email]] || '%') ESCAPE '\\') AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_rel_many_rel.title]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo1.rel_many]]), json_type([[__mm_demo1.rel_many]])='array', FALSE) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] LEFT JOIN `demo2` `__mm_demo1_rel_many_rel` ON [[__mm_demo1_rel_many_rel.id]] = [[__mm_demo1_rel_many.rel]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] LIKE ('%' || [[demo1_rel_one.email]] || '%') ESCAPE '\\')))) AND ((COALESCE([[__collection_demo2.active]], '') = COALESCE([[demo1_rel_many_rel.active]], '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm__collection_demo2.active]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN `demo2` `__mm__collection_demo2` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mlTEST}} LEFT JOIN (SELECT [[__mm_demo1_rel_many_rel.active]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo1.rel_many]]), json_type([[__mm_demo1.rel_many]])='array', FALSE) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] LEFT JOIN `demo2` `__mm_demo1_rel_many_rel` ON [[__mm_demo1_rel_many_rel.id]] = [[__mm_demo1_rel_many.rel]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mrTEST}} WHERE NOT (COALESCE([[__mlTEST.multiMatchValue]], '') = COALESCE([[__mrTEST.multiMatchValue]], ''))))) AND COALESCE([[__collection_demo2.active]], '') = COALESCE([[demo1_rel_many_rel.active]], '') AND (((([[demo1_rel_many.email]] > [[__data_users_rel_many.email]]) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_rel_many.email]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo1.rel_many]]), json_type([[__mm_demo1.rel_many]])='array', FALSE) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mlTEST}} LEFT JOIN (SELECT [[__data_mm_users_rel_many.email]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN `users` `__data_mm_users_rel_many` ON [[__data_mm_users_rel_many.id]] IN ({:p2}, {:p3}) WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mrTEST}} WHERE NOT ([[__mlTEST.multiMatchValue]] > [[__mrTEST.multiMatchValue]]))))) AND ([[demo1_rel_many.emailVisibility]] = TRUE)))",
|
||||
},
|
||||
{
|
||||
"@request.body.arrayable:length fields",
|
||||
@@ -495,14 +495,14 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"self_rel_one.rel_many_cascade.files:length != 7 &&" +
|
||||
"self_rel_one.rel_many_cascade.files:length ?!= 8",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `__data_demo4_self_rel_one` ON [[__data_demo4_self_rel_one.id]]={:p0} LEFT JOIN `demo3` `__data_demo3_rel_many_cascade` ON [[__data_demo3_rel_many_cascade.id]] IN ({:p1}, {:p2}) LEFT JOIN `demo3` `__data_demo3_rel_one_cascade` ON [[__data_demo3_rel_one_cascade.id]]={:p3} LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_one.rel_many_cascade]]) THEN [[demo4_self_rel_one.rel_many_cascade]] ELSE json_array([[demo4_self_rel_one.rel_many_cascade]]) END) `demo4_self_rel_one_rel_many_cascade_je` LEFT JOIN `demo3` `demo4_self_rel_one_rel_many_cascade` ON [[demo4_self_rel_one_rel_many_cascade.id]] = [[demo4_self_rel_one_rel_many_cascade_je.value]] WHERE (json_array_length(CASE WHEN json_valid([[__data_demo4_self_rel_one.self_rel_many]]) THEN [[__data_demo4_self_rel_one.self_rel_many]] ELSE (CASE WHEN [[__data_demo4_self_rel_one.self_rel_many]] = '' OR [[__data_demo4_self_rel_one.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[__data_demo4_self_rel_one.self_rel_many]]) END) END) > {:TEST} AND json_array_length(CASE WHEN json_valid([[__data_demo4_self_rel_one.self_rel_many]]) THEN [[__data_demo4_self_rel_one.self_rel_many]] ELSE (CASE WHEN [[__data_demo4_self_rel_one.self_rel_many]] = '' OR [[__data_demo4_self_rel_one.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[__data_demo4_self_rel_one.self_rel_many]]) END) END) > {:TEST} AND json_array_length(CASE WHEN json_valid([[__data_demo3_rel_many_cascade.files]]) THEN [[__data_demo3_rel_many_cascade.files]] ELSE (CASE WHEN [[__data_demo3_rel_many_cascade.files]] = '' OR [[__data_demo3_rel_many_cascade.files]] IS NULL THEN json_array() ELSE json_array([[__data_demo3_rel_many_cascade.files]]) END) END) < {:TEST} AND ((json_array_length(CASE WHEN json_valid([[__data_demo3_rel_many_cascade.files]]) THEN [[__data_demo3_rel_many_cascade.files]] ELSE (CASE WHEN [[__data_demo3_rel_many_cascade.files]] = '' OR [[__data_demo3_rel_many_cascade.files]] IS NULL THEN json_array() ELSE json_array([[__data_demo3_rel_many_cascade.files]]) END) END) < {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT json_array_length(CASE WHEN json_valid([[__data_mm_demo3_rel_many_cascade.files]]) THEN [[__data_mm_demo3_rel_many_cascade.files]] ELSE (CASE WHEN [[__data_mm_demo3_rel_many_cascade.files]] = '' OR [[__data_mm_demo3_rel_many_cascade.files]] IS NULL THEN json_array() ELSE json_array([[__data_mm_demo3_rel_many_cascade.files]]) END) END) as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN `demo3` `__data_mm_demo3_rel_many_cascade` ON [[__data_mm_demo3_rel_many_cascade.id]] IN ({:p8}, {:p9}) WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] < {:TEST})))) AND json_array_length(CASE WHEN json_valid([[__data_demo3_rel_one_cascade.files]]) THEN [[__data_demo3_rel_one_cascade.files]] ELSE (CASE WHEN [[__data_demo3_rel_one_cascade.files]] = '' OR [[__data_demo3_rel_one_cascade.files]] IS NULL THEN json_array() ELSE json_array([[__data_demo3_rel_one_cascade.files]]) END) END) < {:TEST} AND json_array_length(CASE WHEN json_valid([[demo4_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_one.self_rel_many]] ELSE (CASE WHEN [[demo4_self_rel_one.self_rel_many]] = '' OR [[demo4_self_rel_one.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[demo4_self_rel_one.self_rel_many]]) END) END) = {:TEST} AND json_array_length(CASE WHEN json_valid([[demo4_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_one.self_rel_many]] ELSE (CASE WHEN [[demo4_self_rel_one.self_rel_many]] = '' OR [[demo4_self_rel_one.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[demo4_self_rel_one.self_rel_many]]) END) END) = {:TEST} AND ((json_array_length(CASE WHEN json_valid([[demo4_self_rel_one_rel_many_cascade.files]]) THEN [[demo4_self_rel_one_rel_many_cascade.files]] ELSE (CASE WHEN [[demo4_self_rel_one_rel_many_cascade.files]] = '' OR [[demo4_self_rel_one_rel_many_cascade.files]] IS NULL THEN json_array() ELSE json_array([[demo4_self_rel_one_rel_many_cascade.files]]) END) END) IS NOT {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT json_array_length(CASE WHEN json_valid([[__mm_demo4_self_rel_one_rel_many_cascade.files]]) THEN [[__mm_demo4_self_rel_one_rel_many_cascade.files]] ELSE (CASE WHEN [[__mm_demo4_self_rel_one_rel_many_cascade.files]] = '' OR [[__mm_demo4_self_rel_one_rel_many_cascade.files]] IS NULL THEN json_array() ELSE json_array([[__mm_demo4_self_rel_one_rel_many_cascade.files]]) END) END) as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN `demo4` `__mm_demo4_self_rel_one` ON [[__mm_demo4_self_rel_one.id]] = [[__mm_demo4.self_rel_one]] LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4_self_rel_one.rel_many_cascade]]) THEN [[__mm_demo4_self_rel_one.rel_many_cascade]] ELSE json_array([[__mm_demo4_self_rel_one.rel_many_cascade]]) END) `__mm_demo4_self_rel_one_rel_mLine truncated
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `__data_demo4_self_rel_one` ON [[__data_demo4_self_rel_one.id]]={:p0} LEFT JOIN `demo3` `__data_demo3_rel_many_cascade` ON [[__data_demo3_rel_many_cascade.id]] IN ({:p1}, {:p2}) LEFT JOIN `demo3` `__data_demo3_rel_one_cascade` ON [[__data_demo3_rel_one_cascade.id]]={:p3} LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo4_self_rel_one.rel_many_cascade]]), json_type([[demo4_self_rel_one.rel_many_cascade]])='array', FALSE) THEN [[demo4_self_rel_one.rel_many_cascade]] ELSE json_array([[demo4_self_rel_one.rel_many_cascade]]) END) `demo4_self_rel_one_rel_many_cascade_je` LEFT JOIN `demo3` `demo4_self_rel_one_rel_many_cascade` ON [[demo4_self_rel_one_rel_many_cascade.id]] = [[demo4_self_rel_one_rel_many_cascade_je.value]] WHERE (json_array_length(CASE WHEN iif(json_valid([[__data_demo4_self_rel_one.self_rel_many]]), json_type([[__data_demo4_self_rel_one.self_rel_many]])='array', FALSE) THEN [[__data_demo4_self_rel_one.self_rel_many]] ELSE (CASE WHEN [[__data_demo4_self_rel_one.self_rel_many]] = '' OR [[__data_demo4_self_rel_one.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[__data_demo4_self_rel_one.self_rel_many]]) END) END) > {:TEST} AND json_array_length(CASE WHEN iif(json_valid([[__data_demo4_self_rel_one.self_rel_many]]), json_type([[__data_demo4_self_rel_one.self_rel_many]])='array', FALSE) THEN [[__data_demo4_self_rel_one.self_rel_many]] ELSE (CASE WHEN [[__data_demo4_self_rel_one.self_rel_many]] = '' OR [[__data_demo4_self_rel_one.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[__data_demo4_self_rel_one.self_rel_many]]) END) END) > {:TEST} AND json_array_length(CASE WHEN iif(json_valid([[__data_demo3_rel_many_cascade.files]]), json_type([[__data_demo3_rel_many_cascade.files]])='array', FALSE) THEN [[__data_demo3_rel_many_cascade.files]] ELSE (CASE WHEN [[__data_demo3_rel_many_cascade.files]] = '' OR [[__data_demo3_rel_many_cascade.files]] IS NULL THEN json_array() ELSE json_array([[__data_demo3_rel_many_cascade.files]]) END) END) < {:TEST} AND ((json_array_length(CASE WHEN iif(json_valid([[__data_demo3_rel_many_cascade.files]]), json_type([[__data_demo3_rel_many_cascade.files]])='array', FALSE) THEN [[__data_demo3_rel_many_cascade.files]] ELSE (CASE WHEN [[__data_demo3_rel_many_cascade.files]] = '' OR [[__data_demo3_rel_many_cascade.files]] IS NULL THEN json_array() ELSE json_array([[__data_demo3_rel_many_cascade.files]]) END) END) < {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT json_array_length(CASE WHEN iif(json_valid([[__data_mm_demo3_rel_many_cascade.files]]), json_type([[__data_mm_demo3_rel_many_cascade.files]])='array', FALSE) THEN [[__data_mm_demo3_rel_many_cascade.files]] ELSE (CASE WHEN [[__data_mm_demo3_rel_many_cascade.files]] = '' OR [[__data_mm_demo3_rel_many_cascade.files]] IS NULL THEN json_array() ELSE json_array([[__data_mm_demo3_rel_many_cascade.files]]) END) END) as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN `demo3` `__data_mm_demo3_rel_many_cascade` ON [[__data_mm_demo3_rel_many_cascade.id]] IN ({:p8}, {:p9}) WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] < {:TEST})))) AND json_array_length(CASE WHEN iif(json_valid([[__data_demo3_rel_one_cascade.files]]), json_type([[__data_demo3_rel_one_cascade.files]])='array', FALSE) THEN [[__data_demo3_rel_one_cascade.files]] ELSE (CASE WHEN [[__data_demo3_rel_one_cascade.files]] = '' OR [[__data_demo3_rel_one_cascade.files]] IS NULL THEN json_array() ELSE json_array([[__data_demo3_rel_one_cascade.files]]) END) END) < {:TEST} AND json_array_length(CASE WHEN iif(json_valid([[demo4_self_rel_one.self_rel_many]]), json_type([[demo4_self_rel_one.self_rel_many]])='array', FALSE) THEN [[demo4_self_rel_one.self_rel_many]] ELSE (CASE WHEN [[demo4_self_rel_one.self_rel_many]] = '' OR [[demo4_self_rel_one.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[demo4_self_rel_one.self_rel_many]]) END) END) = {:TEST} AND json_array_length(CASE WHEN iif(json_valid([[demo4_self_rel_one.self_rel_many]]), json_type([[demo4_self_rel_one.self_rel_many]])='array', FALSE) THEN [[demo4_self_rel_one.self_rel_many]] ELSE (CASE WHEN [[demo4_self_rel_one.self_rel_many]] = '' OR [[demo4_self_rel_one.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[demo4_self_rel_one.self_rel_many]]) END) END) = {:TEST} AND ((json_array_length(CASE WHEN iif(json_valid([[demo4_self_rel_one_rel_many_cascade.files]]), json_type([[demo4_self_rel_one_rel_many_cascade.files]])='array', FALSE) THEN [[demo4_self_rel_one_rel_many_cascade.files]] ELSE (CASE WHEN [[demo4_self_rel_one_rel_many_cascade.files]] = '' OR [[demo4_self_rel_one_rel_many_cascade.files]] IS NULL THEN json_array() ELSE json_array([[demo4_self_rel_one_rel_many_cascade.files]]) END) END) IS NOT {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT json_array_length(CASE WHEN iif(json_vLine truncated
|
||||
},
|
||||
{
|
||||
"json_extract and json_array_length COALESCE equal normalizations",
|
||||
"demo4",
|
||||
"json_object.a.b = '' && self_rel_many:length != 2 && json_object.a.b > 3 && self_rel_many:length <= 4",
|
||||
false,
|
||||
"SELECT `demo4`.* FROM `demo4` WHERE ((CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$.a.b') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb.a.b') END) IS {:TEST} AND json_array_length(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE (CASE WHEN [[demo4.self_rel_many]] = '' OR [[demo4.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[demo4.self_rel_many]]) END) END) IS NOT {:TEST} AND (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$.a.b') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb.a.b') END) > {:TEST} AND json_array_length(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE (CASE WHEN [[demo4.self_rel_many]] = '' OR [[demo4.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[demo4.self_rel_many]]) END) END) <= {:TEST})",
|
||||
"SELECT `demo4`.* FROM `demo4` WHERE ((CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$.a.b') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb.a.b') END) IS {:TEST} AND json_array_length(CASE WHEN iif(json_valid([[demo4.self_rel_many]]), json_type([[demo4.self_rel_many]])='array', FALSE) THEN [[demo4.self_rel_many]] ELSE (CASE WHEN [[demo4.self_rel_many]] = '' OR [[demo4.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[demo4.self_rel_many]]) END) END) IS NOT {:TEST} AND (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$.a.b') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb.a.b') END) > {:TEST} AND json_array_length(CASE WHEN iif(json_valid([[demo4.self_rel_many]]), json_type([[demo4.self_rel_many]])='array', FALSE) THEN [[demo4.self_rel_many]] ELSE (CASE WHEN [[demo4.self_rel_many]] = '' OR [[demo4.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[demo4.self_rel_many]]) END) END) <= {:TEST})",
|
||||
},
|
||||
{
|
||||
"json field equal normalization checks",
|
||||
@@ -516,7 +516,7 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"self_rel_many.json_object = '' || null = self_rel_many.json_object ||" +
|
||||
"self_rel_many.json_object = self_rel_many.json_object",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] WHERE ((CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS {:TEST} OR (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS NOT {:TEST} OR {:TEST} IS (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR {:TEST} IS NOT (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS NULL OR (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS NOT NULL OR NULL IS (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR NULL IS NOT (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS 1 OR (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS NOT 1 OR 1 IS (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR 1 IS NOT (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS NOT (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS [[demo4.title]] OR [[demo4.title]] IS NOT (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR (((CASE WHEN json_valid([[demo4_self_rel_many.json_object]]) THEN JSON_EXTRACT([[demo4_self_rel_many.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4_self_rel_many.json_object]]), '$.pb') END) IS {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT (CASE WHEN json_valid([[__mm_demo4_self_rel_many.json_object]]) THEN JSON_EXTRACT([[__mm_demo4_self_rel_many.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[__mm_demo4_self_rel_many.json_object]]), '$.pb') END) as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_many]]) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] IS {:TEST})))) OR ((NULL IS (CASE WHEN json_valid([[demo4_self_rel_many.json_object]]) THEN JSON_EXTRACT([[demo4_self_rel_many.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4_self_rel_many.json_object]]), '$.pb') END)) AND (NOT EXISTS (SELECT 1 FROM (SELECT (CASE WHEN json_valid([[__mm_demo4_self_rel_many.json_object]]) THEN JSON_EXTRACT([[__mm_demo4_self_rel_many.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[__mm_demo4_self_rel_many.json_object]]), '$.pb') END) as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN jsonLine truncated
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo4.self_rel_many]]), json_type([[demo4.self_rel_many]])='array', FALSE) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] WHERE ((CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS {:TEST} OR (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS NOT {:TEST} OR {:TEST} IS (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR {:TEST} IS NOT (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS NULL OR (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS NOT NULL OR NULL IS (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR NULL IS NOT (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS 1 OR (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS NOT 1 OR 1 IS (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR 1 IS NOT (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS NOT (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) IS [[demo4.title]] OR [[demo4.title]] IS NOT (CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb') END) OR (((CASE WHEN json_valid([[demo4_self_rel_many.json_object]]) THEN JSON_EXTRACT([[demo4_self_rel_many.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4_self_rel_many.json_object]]), '$.pb') END) IS {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT (CASE WHEN json_valid([[__mm_demo4_self_rel_many.json_object]]) THEN JSON_EXTRACT([[__mm_demo4_self_rel_many.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[__mm_demo4_self_rel_many.json_object]]), '$.pb') END) as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN iif(json_valid([[__mm_demo4.self_rel_many]]), json_type([[__mm_demo4.self_rel_many]])='array', FALSE) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] IS {:TEST})))) OR ((NULL IS (CASE WHEN json_valid([[demo4_self_rel_many.json_object]]) THEN JSON_EXTRACT([[demo4_self_rel_many.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[demo4_self_rel_many.json_object]]), '$.pb') END)) AND (NOT EXISTS (SELECT 1 FROM (SELECT (CASE WHEN json_valid([[__mm_demo4_self_rel_many.json_object]]) THEN JSON_EXTRACT([[__mm_demo4_self_rel_many.json_object]], '$') ELSE JSON_EXTRACT(json_object('pb', [[__mm_demo4_self_rel_Line truncated
|
||||
},
|
||||
{
|
||||
"geoPoint props access",
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core/validators"
|
||||
"github.com/pocketbase/pocketbase/tools/dbutils"
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||
"github.com/pocketbase/pocketbase/tools/hook"
|
||||
"github.com/pocketbase/pocketbase/tools/inflector"
|
||||
@@ -1530,8 +1531,8 @@ func cascadeRecordDelete(app App, mainRecord *Record, refs map[*Collection][]Fie
|
||||
query.AndWhere(dbx.HashExp{prefixedFieldName: mainRecord.Id})
|
||||
} else {
|
||||
query.AndWhere(dbx.Exists(dbx.NewExp(fmt.Sprintf(
|
||||
`SELECT 1 FROM json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END) {{__je__}} WHERE [[__je__.value]]={:jevalue}`,
|
||||
prefixedFieldName, prefixedFieldName, prefixedFieldName,
|
||||
`SELECT 1 FROM %s {{__je__}} WHERE [[__je__.value]]={:jevalue}`,
|
||||
dbutils.JSONEach(prefixedFieldName),
|
||||
), dbx.Params{
|
||||
"jevalue": mainRecord.Id,
|
||||
})))
|
||||
|
||||
@@ -879,7 +879,7 @@ func TestRecordGetInt(t *testing.T) {
|
||||
{123, 123},
|
||||
{2.4, 2},
|
||||
{"123", 123},
|
||||
{"123.5", 0},
|
||||
{"123.5", 123},
|
||||
{false, 0},
|
||||
{true, 1},
|
||||
}
|
||||
@@ -2254,13 +2254,13 @@ func TestRecordDelete(t *testing.T) {
|
||||
app.NonconcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
calledQueries = append(calledQueries, sql)
|
||||
}
|
||||
app.DB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
app.ConcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
calledQueries = append(calledQueries, sql)
|
||||
}
|
||||
app.NonconcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
||||
calledQueries = append(calledQueries, sql)
|
||||
}
|
||||
app.DB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
||||
app.ConcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
||||
calledQueries = append(calledQueries, sql)
|
||||
}
|
||||
rec3, _ := app.FindRecordById("users", "oap640cot4yru2s")
|
||||
@@ -2280,7 +2280,7 @@ func TestRecordDelete(t *testing.T) {
|
||||
}
|
||||
// ensure that the json rel fields were prefixed
|
||||
joinedQueries := strings.Join(calledQueries, " ")
|
||||
expectedRelManyPart := "SELECT `demo1`.* FROM `demo1` WHERE EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid([[demo1.rel_many]]) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END) {{__je__}} WHERE [[__je__.value]]='"
|
||||
expectedRelManyPart := "SELECT `demo1`.* FROM `demo1` WHERE EXISTS (SELECT 1 FROM json_each(CASE WHEN iif(json_valid([[demo1.rel_many]]), json_type([[demo1.rel_many]])='array', FALSE) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END) {{__je__}} WHERE [[__je__.value]]='"
|
||||
if !strings.Contains(joinedQueries, expectedRelManyPart) {
|
||||
t.Fatalf("(rec3) Expected the cascade delete to call the query \n%v, got \n%v", expectedRelManyPart, calledQueries)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func (app *BaseApp) RecordQuery(collectionModelOrIdentifier any) *dbx.SelectQuer
|
||||
tableName = "@@__invalidCollectionModelOrIdentifier"
|
||||
}
|
||||
|
||||
query := app.DB().Select(app.DB().QuoteSimpleColumnName(tableName) + ".*").From(tableName)
|
||||
query := app.ConcurrentDB().Select(app.ConcurrentDB().QuoteSimpleColumnName(tableName) + ".*").From(tableName)
|
||||
|
||||
// in case of an error attach a new context and cancel it immediately with the error
|
||||
if collectionErr != nil {
|
||||
|
||||
@@ -103,7 +103,7 @@ func (app *BaseApp) expandRecords(records []*Record, expandPath string, fetchFun
|
||||
// add the related id(s) as a dynamic relation field value to
|
||||
// allow further expand checks at later stage in a more unified manner
|
||||
prepErr := func() error {
|
||||
q := app.DB().Select("id").
|
||||
q := app.ConcurrentDB().Select("id").
|
||||
From(indirectRel.Name).
|
||||
Limit(1000) // the limit is arbitrary chosen and may change in the future
|
||||
|
||||
|
||||
@@ -704,3 +704,13 @@ func (c RateLimitRule) Validate() error {
|
||||
func (c RateLimitRule) DurationTime() time.Duration {
|
||||
return time.Duration(c.Duration) * time.Second
|
||||
}
|
||||
|
||||
// String returns a string representation of the rule.
|
||||
func (c RateLimitRule) String() string {
|
||||
raw, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return c.Label // extremely rare case
|
||||
}
|
||||
|
||||
return string(raw)
|
||||
}
|
||||
@@ -687,7 +687,7 @@ func TestRateLimitsFindRateLimitRule(t *testing.T) {
|
||||
func TestRateLimitRuleValidate(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
config core.RateLimitRule
|
||||
rule core.RateLimitRule
|
||||
expectedErrors []string
|
||||
}{
|
||||
{
|
||||
@@ -784,7 +784,7 @@ func TestRateLimitRuleValidate(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := s.config.Validate()
|
||||
result := s.rule.Validate()
|
||||
|
||||
tests.TestValidationErrors(t, result, s.expectedErrors)
|
||||
})
|
||||
@@ -793,7 +793,7 @@ func TestRateLimitRuleValidate(t *testing.T) {
|
||||
|
||||
func TestRateLimitRuleDurationTime(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
config core.RateLimitRule
|
||||
rule core.RateLimitRule
|
||||
expected time.Duration
|
||||
}{
|
||||
{core.RateLimitRule{}, 0 * time.Second},
|
||||
@@ -801,8 +801,8 @@ func TestRateLimitRuleDurationTime(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d_%d", i, s.config.Duration), func(t *testing.T) {
|
||||
result := s.config.DurationTime()
|
||||
t.Run(fmt.Sprintf("%d_%d", i, s.rule.Duration), func(t *testing.T) {
|
||||
result := s.rule.DurationTime()
|
||||
|
||||
if result != s.expected {
|
||||
t.Fatalf("Expected duration %d, got %d", s.expected, result)
|
||||
@@ -810,3 +810,37 @@ func TestRateLimitRuleDurationTime(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimitRuleString(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
rule core.RateLimitRule
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"empty",
|
||||
core.RateLimitRule{},
|
||||
`{"label":"","audience":"","duration":0,"maxRequests":0}`,
|
||||
},
|
||||
{
|
||||
"all fields",
|
||||
core.RateLimitRule{
|
||||
Label: "POST /a/b/",
|
||||
Duration: 1,
|
||||
MaxRequests: 2,
|
||||
Audience: core.RateLimitRuleAudienceAuth,
|
||||
},
|
||||
`{"label":"POST /a/b/","audience":"@auth","duration":1,"maxRequests":2}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := s.rule.String()
|
||||
|
||||
if result != s.expected {
|
||||
t.Fatalf("Expected string\n%s\ngot\n%s", s.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -215,7 +215,7 @@ func defaultViewField(name string) Field {
|
||||
}
|
||||
}
|
||||
|
||||
var castRegex = regexp.MustCompile(`(?i)^cast\s*\(.*\s+as\s+(\w+)\s*\)$`)
|
||||
var castRegex = regexp.MustCompile(`(?is)^cast\s*\(.*\s+as\s+(\w+)\s*\)$`)
|
||||
|
||||
func parseQueryToFields(app App, selectQuery string) (map[string]*queryField, error) {
|
||||
p := new(identifiersParser)
|
||||
|
||||
@@ -394,6 +394,38 @@ func TestCreateViewFields(t *testing.T) {
|
||||
"max": core.FieldTypeJSON,
|
||||
},
|
||||
},
|
||||
{
|
||||
"query with multiline cast",
|
||||
`select
|
||||
id,
|
||||
cast(
|
||||
(
|
||||
case
|
||||
when count(a.id) = 1 then 21
|
||||
when count(a.id) = 2 then 18
|
||||
else 0
|
||||
end
|
||||
) as int
|
||||
) as cast_int
|
||||
from demo1 a`,
|
||||
false,
|
||||
map[string]string{
|
||||
"id": core.FieldTypeText,
|
||||
"cast_int": core.FieldTypeNumber,
|
||||
},
|
||||
},
|
||||
{
|
||||
"query with case-insensitive and extra-spaced cast",
|
||||
`select
|
||||
id,
|
||||
CaSt( a.id aS iNt ) as cast_int
|
||||
from demo1 a`,
|
||||
false,
|
||||
map[string]string{
|
||||
"id": core.FieldTypeText,
|
||||
"cast_int": core.FieldTypeNumber,
|
||||
},
|
||||
},
|
||||
{
|
||||
"query with reserved auth collection fields",
|
||||
`
|
||||
|
||||
@@ -227,7 +227,7 @@ func (form *RecordUpsert) DrySubmit(callback func(txApp core.App, drySavedRecord
|
||||
|
||||
app := form.app.UnsafeWithoutHooks()
|
||||
|
||||
_, isTransactional := app.DB().(*dbx.Tx)
|
||||
isTransactional := app.IsTransactional()
|
||||
if !isTransactional {
|
||||
return app.RunInTransaction(func(txApp core.App) error {
|
||||
tx, ok := txApp.DB().(*dbx.Tx)
|
||||
|
||||
@@ -9,20 +9,20 @@ require (
|
||||
github.com/dop251/goja_nodejs v0.0.0-20250314160716-c55ecee183c0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.8
|
||||
github.com/gabriel-vasile/mimetype v1.4.9
|
||||
github.com/ganigeorgiev/fexpr v0.5.0
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3
|
||||
github.com/pocketbase/dbx v1.11.0
|
||||
github.com/pocketbase/tygoja v0.0.0-20250103200817-ca580d8c5119
|
||||
github.com/spf13/cast v1.7.1
|
||||
github.com/spf13/cast v1.9.2
|
||||
github.com/spf13/cobra v1.9.1
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/image v0.26.0
|
||||
golang.org/x/net v0.39.0
|
||||
golang.org/x/oauth2 v0.29.0
|
||||
golang.org/x/sync v0.13.0
|
||||
modernc.org/sqlite v1.37.0
|
||||
golang.org/x/crypto v0.40.0
|
||||
golang.org/x/image v0.29.0
|
||||
golang.org/x/net v0.42.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.16.0
|
||||
modernc.org/sqlite v1.38.2
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -38,13 +38,13 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/tools v0.32.0 // indirect
|
||||
modernc.org/libc v1.62.1 // indirect
|
||||
github.com/spf13/pflag v1.0.7 // indirect
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
|
||||
golang.org/x/mod v0.26.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.35.0 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.9.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
@@ -26,8 +26,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.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk=
|
||||
github.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
||||
@@ -36,8 +36,8 @@ github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TC
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@@ -68,43 +68,44 @@ 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.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
||||
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
|
||||
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
|
||||
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -112,26 +113,28 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic=
|
||||
modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU=
|
||||
modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s=
|
||||
modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
|
||||
modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
||||
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
expectedDriverVersion = "v1.37.0"
|
||||
expectedLibcVersion = "v1.62.1"
|
||||
expectedDriverVersion = "v1.38.2"
|
||||
expectedLibcVersion = "v1.66.3"
|
||||
|
||||
// ModerncDepsCheckHookId is the id of the hook that performs the modernc.org/* deps checks.
|
||||
// It could be used for removing/unbinding the hook if you don't want the checks.
|
||||
|
||||
@@ -309,6 +309,44 @@ func baseBinds(vm *goja.Runtime) {
|
||||
return string(bodyBytes), nil
|
||||
})
|
||||
|
||||
// note: throw only on reader error
|
||||
vm.Set("toBytes", func(raw any, maxReaderBytes int) ([]byte, error) {
|
||||
switch v := raw.(type) {
|
||||
case nil:
|
||||
return []byte{}, nil
|
||||
case string:
|
||||
return []byte(v), nil
|
||||
case []byte:
|
||||
return v, nil
|
||||
case types.JSONRaw:
|
||||
return v, nil
|
||||
case io.Reader:
|
||||
if maxReaderBytes == 0 {
|
||||
maxReaderBytes = router.DefaultMaxMemory
|
||||
}
|
||||
|
||||
limitReader := io.LimitReader(v, int64(maxReaderBytes))
|
||||
|
||||
return io.ReadAll(limitReader)
|
||||
default:
|
||||
b, err := cast.ToUint8SliceE(v)
|
||||
if err == nil {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
str, err := cast.ToStringE(v)
|
||||
if err == nil {
|
||||
return []byte(str), nil
|
||||
}
|
||||
|
||||
// as a last attempt try to json encode the value
|
||||
rawBytes, _ := json.Marshal(raw)
|
||||
|
||||
return rawBytes, nil
|
||||
}
|
||||
})
|
||||
|
||||
// note: throw only on reader error
|
||||
vm.Set("toString", func(raw any, maxReaderBytes int) (string, error) {
|
||||
switch v := raw.(type) {
|
||||
case io.Reader:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package jsvm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||
"github.com/pocketbase/pocketbase/tools/mailer"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
@@ -44,7 +46,7 @@ func TestBaseBindsCount(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
|
||||
testBindsCount(vm, "this", 34, t)
|
||||
testBindsCount(vm, "this", 35, t)
|
||||
}
|
||||
|
||||
func TestBaseBindsSleep(t *testing.T) {
|
||||
@@ -83,7 +85,7 @@ func TestBaseBindsReaderToString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsToStringAndToBytes(t *testing.T) {
|
||||
func TestBaseBindsToString(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
vm.Set("scenarios", []struct {
|
||||
@@ -106,10 +108,49 @@ func TestBaseBindsToStringAndToBytes(t *testing.T) {
|
||||
|
||||
_, err := vm.RunString(`
|
||||
for (let s of scenarios) {
|
||||
let result = toString(s.value)
|
||||
let str = toString(s.value)
|
||||
if (str != s.expected) {
|
||||
throw new Error('[' + s.name + '] Expected string ' + s.expected + ', got ' + str);
|
||||
}
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if (result != s.expected) {
|
||||
throw new Error('[' + s.name + '] Expected string ' + s.expected + ', got ' + result);
|
||||
func TestBaseBindsToBytes(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
vm.Set("bytesEqual", bytes.Equal)
|
||||
vm.Set("scenarios", []struct {
|
||||
Name string
|
||||
Value any
|
||||
Expected []byte
|
||||
}{
|
||||
{"null", nil, []byte{}},
|
||||
{"string", "test", []byte("test")},
|
||||
{"number", -12.4, []byte("-12.4")},
|
||||
{"bool", true, []byte("true")},
|
||||
{"arr", []int{1, 2, 3}, []byte{1, 2, 3}},
|
||||
{"jsonraw", types.JSONRaw{1, 2, 3}, []byte{1, 2, 3}},
|
||||
{"reader", strings.NewReader("test"), []byte("test")},
|
||||
{"obj", map[string]any{"test": 123}, []byte(`{"test":123}`)},
|
||||
{"struct", struct {
|
||||
Name string
|
||||
private string
|
||||
}{Name: "123", private: "456"}, []byte(`{"Name":"123"}`)},
|
||||
})
|
||||
|
||||
_, err := vm.RunString(`
|
||||
for (let s of scenarios) {
|
||||
let b = toBytes(s.value)
|
||||
if (!Array.isArray(b)) {
|
||||
throw new Error('[' + s.name + '] Expected toBytes to return an array');
|
||||
}
|
||||
|
||||
if (!bytesEqual(b, s.expected)) {
|
||||
throw new Error('[' + s.name + '] Expected bytes ' + s.expected + ', got ' + b);
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
+4141
-3792
File diff suppressed because it is too large.
Load diff
@@ -186,14 +186,46 @@ declare function readerToString(reader: any, maxBytes?: number): string;
|
||||
* // io.Reader
|
||||
* const ex1 = toString(e.request.body)
|
||||
*
|
||||
* // slice of bytes ("hello")
|
||||
* const ex2 = toString([104 101 108 108 111])
|
||||
* // slice of bytes
|
||||
* const ex2 = toString([104 101 108 108 111]) // "hello"
|
||||
*
|
||||
* // null
|
||||
* const ex3 = toString(null) // ""
|
||||
* ` + "```" + `
|
||||
*
|
||||
* @group PocketBase
|
||||
*/
|
||||
declare function toString(val: any, maxBytes?: number): string;
|
||||
|
||||
/**
|
||||
* toBytes converts the specified value into a bytes slice.
|
||||
*
|
||||
* Support optional second maxBytes argument to limit the max read bytes
|
||||
* when the value is a io.Reader (default to 32MB).
|
||||
*
|
||||
* Types that don't have Go slice representation (bool, objects, etc.)
|
||||
* are serialized to UTF8 string and its bytes slice is returned.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ` + "```" + `js
|
||||
* // io.Reader
|
||||
* const ex1 = toBytes(e.request.body)
|
||||
*
|
||||
* // string
|
||||
* const ex2 = toBytes("hello") // [104 101 108 108 111]
|
||||
*
|
||||
* // object (the same as the string '{"test":1}')
|
||||
* const ex3 = toBytes({"test":1}) // [123 34 116 101 115 116 34 58 49 125]
|
||||
*
|
||||
* // null
|
||||
* const ex4 = toBytes(null) // []
|
||||
* ` + "```" + `
|
||||
*
|
||||
* @group PocketBase
|
||||
*/
|
||||
declare function toBytes(val: any, maxBytes?: number): Array<number>;
|
||||
|
||||
/**
|
||||
* sleep pauses the current goroutine for at least the specified user duration (in ms).
|
||||
* A zero or negative duration returns immediately.
|
||||
@@ -227,7 +259,9 @@ declare function arrayOf<T>(model: T): Array<T>;
|
||||
/**
|
||||
* DynamicModel creates a new dynamic model with fields from the provided data shape.
|
||||
*
|
||||
* Note that in order to use 0 as double/float initialization number you have to use negative zero (` + "`-0`" + `).
|
||||
* Caveats:
|
||||
* - In order to use 0 as double/float initialization number you have to negate it (` + "`-0`" + `).
|
||||
* - You need to use lowerCamelCase when accessing the model fields (e.g. ` + "`model.roles`" + ` and not ` + "`model.Roles`" + `).
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
@@ -237,7 +271,7 @@ declare function arrayOf<T>(model: T): Array<T>;
|
||||
* age: 0, // int64
|
||||
* totalSpent: -0, // float64
|
||||
* active: false,
|
||||
* roles: [],
|
||||
* Roles: [], // maps to "Roles" in the DB/JSON but the prop would be accessible via "model.roles"
|
||||
* meta: {}
|
||||
* })
|
||||
* ` + "```" + `
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ type ApiScenario struct {
|
||||
//
|
||||
// map[string]int{ "*": 0 } // no hook events were fired
|
||||
// map[string]int{ "*": 0, "EventA": 2 } // no hook events, except EventA were fired
|
||||
// map[string]int{ EventA": 2, "EventB": 0 } // ensures that EventA was fired exactly 2 times and EventB exactly 0 times.
|
||||
// map[string]int{ "EventA": 2, "EventB": 0 } // ensures that EventA was fired exactly 2 times and EventB exactly 0 times.
|
||||
ExpectedEvents map[string]int
|
||||
|
||||
// test hooks
|
||||
|
||||
+10
-1
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func TestProvidersCount(t *testing.T) {
|
||||
expected := 30
|
||||
expected := 31
|
||||
|
||||
if total := len(auth.Providers); total != expected {
|
||||
t.Fatalf("Expected %d providers, got %d", expected, total)
|
||||
@@ -279,6 +279,15 @@ func TestNewProviderByName(t *testing.T) {
|
||||
t.Error("Expected to be instance of *auth.Wakatime")
|
||||
}
|
||||
|
||||
// box
|
||||
p, err = auth.NewProviderByName(auth.NameBox)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Box); !ok {
|
||||
t.Error("Expected to be instance of *auth.Box")
|
||||
}
|
||||
|
||||
// linear
|
||||
p, err = auth.NewProviderByName(auth.NameLinear)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameBox] = wrapFactory(NewBoxProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Box)(nil)
|
||||
|
||||
// NameBox is the unique name of the Box provider.
|
||||
const NameBox = "box"
|
||||
|
||||
// Box is an auth provider for Box.
|
||||
type Box struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewBoxProvider creates a new Box provider instance with some defaults.
|
||||
func NewBoxProvider() *Box {
|
||||
return &Box{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Box",
|
||||
pkce: true,
|
||||
scopes: []string{"root_readonly"},
|
||||
authURL: "https://account.box.com/api/oauth2/authorize",
|
||||
tokenURL: "https://api.box.com/oauth2/token",
|
||||
userInfoURL: "https://api.box.com/2.0/users/me",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the Box's user API.
|
||||
//
|
||||
// API reference: https://developer.box.com/reference/get-users-me/
|
||||
func (p *Box) 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 {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Login string `json:"login"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Status string `json:"status"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if extracted.Status != "active" {
|
||||
return nil, fmt.Errorf("Box user account is not active (status: %q)", extracted.Status)
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
AvatarURL: extracted.AvatarURL,
|
||||
Email: extracted.Login, // Box requires verified email for OAuth authorization
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
+10
-11
@@ -26,24 +26,25 @@ type Twitter struct {
|
||||
func NewTwitterProvider() *Twitter {
|
||||
return &Twitter{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Twitter",
|
||||
displayName: "X/Twitter",
|
||||
pkce: true,
|
||||
scopes: []string{
|
||||
"users.read",
|
||||
"users.email",
|
||||
|
||||
// we don't actually use this scope, but for some reason it is required by the `/2/users/me` endpoint
|
||||
// (see https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me)
|
||||
// we don't actually use this scope, but for some reason the `/2/users/me` endpoint fails with 403 without it
|
||||
// (see https://docs.x.com/fundamentals/authentication/guides/v2-authentication-mapping#x-api-v2-authentication-mapping)
|
||||
"tweet.read",
|
||||
},
|
||||
authURL: "https://twitter.com/i/oauth2/authorize",
|
||||
tokenURL: "https://api.twitter.com/2/oauth2/token",
|
||||
userInfoURL: "https://api.twitter.com/2/users/me?user.fields=id,name,username,profile_image_url",
|
||||
authURL: "https://x.com/i/oauth2/authorize",
|
||||
tokenURL: "https://api.x.com/2/oauth2/token",
|
||||
userInfoURL: "https://api.x.com/2/users/me?user.fields=id,name,username,profile_image_url,confirmed_email",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the Twitter's user api.
|
||||
//
|
||||
// API reference: https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me
|
||||
// API reference: https://docs.x.com/x-api/users/user-lookup-me
|
||||
func (p *Twitter) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
@@ -60,11 +61,8 @@ func (p *Twitter) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"confirmed_email"`
|
||||
ProfileImageURL string `json:"profile_image_url"`
|
||||
|
||||
// NB! At the time of writing, Twitter OAuth2 doesn't support returning the user email address
|
||||
// (see https://twittercommunity.com/t/which-api-to-get-user-after-oauth2-authorization/162417/33)
|
||||
// Email string `json:"email"`
|
||||
} `json:"data"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
@@ -75,6 +73,7 @@ func (p *Twitter) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
Id: extracted.Data.Id,
|
||||
Name: extracted.Data.Name,
|
||||
Username: extracted.Data.Username,
|
||||
Email: extracted.Data.Email,
|
||||
AvatarURL: extracted.Data.ProfileImageURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
|
||||
@@ -8,9 +8,11 @@ import (
|
||||
// JSONEach returns JSON_EACH SQLite string expression with
|
||||
// some normalizations for non-json columns.
|
||||
func JSONEach(column string) string {
|
||||
// note: we are not using the new and shorter "if(x,y)" syntax for
|
||||
// compatibility with custom drivers that use older SQLite version
|
||||
return fmt.Sprintf(
|
||||
`json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END)`,
|
||||
column, column, column,
|
||||
`json_each(CASE WHEN iif(json_valid([[%s]]), json_type([[%s]])='array', FALSE) THEN [[%s]] ELSE json_array([[%s]]) END)`,
|
||||
column, column, column, column,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,9 +23,11 @@ func JSONEach(column string) string {
|
||||
//
|
||||
// Returns 0 for empty string or NULL column values.
|
||||
func JSONArrayLength(column string) string {
|
||||
// note: we are not using the new and shorter "if(x,y)" syntax for
|
||||
// compatibility with custom drivers that use older SQLite version
|
||||
return fmt.Sprintf(
|
||||
`json_array_length(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE (CASE WHEN [[%s]] = '' OR [[%s]] IS NULL THEN json_array() ELSE json_array([[%s]]) END) END)`,
|
||||
column, column, column, column, column,
|
||||
`json_array_length(CASE WHEN iif(json_valid([[%s]]), json_type([[%s]])='array', FALSE) THEN [[%s]] ELSE (CASE WHEN [[%s]] = '' OR [[%s]] IS NULL THEN json_array() ELSE json_array([[%s]]) END) END)`,
|
||||
column, column, column, column, column, column,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
func TestJSONEach(t *testing.T) {
|
||||
result := dbutils.JSONEach("a.b")
|
||||
|
||||
expected := "json_each(CASE WHEN json_valid([[a.b]]) THEN [[a.b]] ELSE json_array([[a.b]]) END)"
|
||||
expected := "json_each(CASE WHEN iif(json_valid([[a.b]]), json_type([[a.b]])='array', FALSE) THEN [[a.b]] ELSE json_array([[a.b]]) END)"
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf("Expected\n%v\ngot\n%v", expected, result)
|
||||
@@ -19,7 +19,7 @@ func TestJSONEach(t *testing.T) {
|
||||
func TestJSONArrayLength(t *testing.T) {
|
||||
result := dbutils.JSONArrayLength("a.b")
|
||||
|
||||
expected := "json_array_length(CASE WHEN json_valid([[a.b]]) THEN [[a.b]] ELSE (CASE WHEN [[a.b]] = '' OR [[a.b]] IS NULL THEN json_array() ELSE json_array([[a.b]]) END) END)"
|
||||
expected := "json_array_length(CASE WHEN iif(json_valid([[a.b]]), json_type([[a.b]])='array', FALSE) THEN [[a.b]] ELSE (CASE WHEN [[a.b]] = '' OR [[a.b]] IS NULL THEN json_array() ELSE json_array([[a.b]]) END) END)"
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf("Expected\n%v\ngot\n%v", expected, result)
|
||||
|
||||
@@ -387,11 +387,10 @@ func (b *Bucket) Attributes(ctx context.Context, key string) (_ *Attributes, err
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Exists returns true if a blob exists at key, false if it does not exist, or
|
||||
// an error.
|
||||
// Exists returns true if a blob exists at key, false if it does not exist, or an error.
|
||||
//
|
||||
// It is a shortcut for calling Attributes and checking if it returns an error
|
||||
// with code ErrNotFound.
|
||||
// It is a shortcut for calling Attributes and checking if it returns
|
||||
// an error with code ErrNotFound.
|
||||
func (b *Bucket) Exists(ctx context.Context, key string) (bool, error) {
|
||||
_, err := b.Attributes(ctx, key)
|
||||
if err == nil {
|
||||
@@ -419,11 +418,8 @@ func (b *Bucket) NewReader(ctx context.Context, key string) (*Reader, error) {
|
||||
//
|
||||
// Note that ctx is used for all reads performed during the lifetime of the reader.
|
||||
//
|
||||
// If the blob does not exist, NewRangeReader returns an error for which
|
||||
// gcerrors.Code will return gcerrors.NotFound. Exists is a lighter-weight way
|
||||
// to check for existence.
|
||||
//
|
||||
// A nil ReaderOptions is treated the same as the zero value.
|
||||
// If the blob does not exist, NewRangeReader returns an ErrNotFound.
|
||||
// Exists is a lighter-weight wayto check for existence.
|
||||
//
|
||||
// The caller must call Close on the returned Reader when done reading.
|
||||
func (b *Bucket) NewRangeReader(ctx context.Context, key string, offset, length int64) (_ *Reader, err error) {
|
||||
@@ -656,8 +652,7 @@ func (b *Bucket) NewWriter(ctx context.Context, key string, opts *WriterOptions)
|
||||
// Copy the blob stored at srcKey to dstKey.
|
||||
// A nil CopyOptions is treated the same as the zero value.
|
||||
//
|
||||
// If the source blob does not exist, Copy returns an error for which
|
||||
// gcerrors.Code will return gcerrors.NotFound.
|
||||
// If the source blob does not exist, Copy returns an ErrNotFound.
|
||||
//
|
||||
// If the destination blob already exists, it is overwritten.
|
||||
func (b *Bucket) Copy(ctx context.Context, dstKey, srcKey string) (err error) {
|
||||
@@ -699,6 +694,8 @@ func (b *Bucket) Delete(ctx context.Context, key string) (err error) {
|
||||
}
|
||||
|
||||
// Close releases any resources used for the bucket.
|
||||
//
|
||||
// @todo Consider removing it.
|
||||
func (b *Bucket) Close() error {
|
||||
b.mu.Lock()
|
||||
prev := b.closed
|
||||
|
||||
@@ -40,8 +40,7 @@ type Driver interface {
|
||||
NormalizeError(err error) error
|
||||
|
||||
// Attributes returns attributes for the blob. If the specified object does
|
||||
// not exist, Attributes must return an error for which ErrorCode returns
|
||||
// gcerrors.NotFound.
|
||||
// not exist, Attributes must return an ErrNotFound.
|
||||
// The portable type will not modify the returned Attributes.
|
||||
Attributes(ctx context.Context, key string) (*Attributes, error)
|
||||
|
||||
@@ -51,20 +50,12 @@ type Driver interface {
|
||||
// to recently written or deleted objects. That is to say, there is no
|
||||
// guarantee that an object that's been written will immediately be returned
|
||||
// from ListPaged.
|
||||
// opts is guaranteed to be non-nil.
|
||||
ListPaged(ctx context.Context, opts *ListOptions) (*ListPage, error)
|
||||
|
||||
// NewRangeReader returns a Reader that reads part of an object, reading at
|
||||
// most length bytes starting at the given offset. If length is negative, it
|
||||
// will read until the end of the object. If the specified object does not
|
||||
// exist, NewRangeReader must return an error for which ErrorCode returns
|
||||
// gcerrors.NotFound.
|
||||
// opts is guaranteed to be non-nil.
|
||||
//
|
||||
// The returned Reader *may* also implement Downloader if the underlying
|
||||
// implementation can take advantage of that. The Download call is guaranteed
|
||||
// to be the only call to the Reader. For such readers, offset will always
|
||||
// be 0 and length will always be -1.
|
||||
// exist, NewRangeReader must return an ErrNotFound.
|
||||
NewRangeReader(ctx context.Context, key string, offset, length int64) (DriverReader, error)
|
||||
|
||||
// NewTypedWriter returns Writer that writes to an object associated with key.
|
||||
@@ -89,17 +80,13 @@ type Driver interface {
|
||||
|
||||
// Copy copies the object associated with srcKey to dstKey.
|
||||
//
|
||||
// If the source object does not exist, Copy must return an error for which
|
||||
// ErrorCode returns gcerrors.NotFound.
|
||||
// If the source object does not exist, Copy must return an ErrNotFound.
|
||||
//
|
||||
// If the destination object already exists, it should be overwritten.
|
||||
//
|
||||
// opts is guaranteed to be non-nil.
|
||||
Copy(ctx context.Context, dstKey, srcKey string) error
|
||||
|
||||
// Delete deletes the object associated with key. If the specified object does
|
||||
// not exist, Delete must return an error for which ErrorCode returns
|
||||
// gcerrors.NotFound.
|
||||
// not exist, Delete must return an ErrNotFound.
|
||||
Delete(ctx context.Context, key string) error
|
||||
|
||||
// Close cleans up any resources used by the Bucket. Once Close is called,
|
||||
|
||||
@@ -173,7 +173,7 @@ func (r *Reader) WriteTo(w io.Writer) (int64, error) {
|
||||
func readFromWriteTo(r io.Reader, w io.Writer) (int64, int64, error) {
|
||||
// Note: can't use io.Copy because it will try to use r.WriteTo
|
||||
// or w.WriteTo, which is recursive in this context.
|
||||
buf := make([]byte, 1024)
|
||||
buf := make([]byte, 4096)
|
||||
var totalRead, totalWritten int64
|
||||
for {
|
||||
numRead, rerr := r.Read(buf)
|
||||
|
||||
@@ -176,6 +176,18 @@ func (r *bytesReadSeekCloser) Close() error {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
var _ FileReader = (openFuncAsReader)(nil)
|
||||
|
||||
// openFuncAsReader defines a FileReader from a bare Open function.
|
||||
type openFuncAsReader func() (io.ReadSeekCloser, error)
|
||||
|
||||
// Open implements the [filesystem.FileReader] interface.
|
||||
func (r openFuncAsReader) Open() (io.ReadSeekCloser, error) {
|
||||
return r()
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
var extInvalidCharsRegex = regexp.MustCompile(`[^\w\.\*\-\+\=\#]+`)
|
||||
|
||||
const randomAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/fatih/color"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem/blob"
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem/internal/fileblob"
|
||||
@@ -29,6 +31,8 @@ import (
|
||||
// note: the same as blob.ErrNotFound for backward compatibility with earlier versions
|
||||
var ErrNotFound = blob.ErrNotFound
|
||||
|
||||
const metadataOriginalName = "original-filename"
|
||||
|
||||
type System struct {
|
||||
ctx context.Context
|
||||
bucket *blob.Bucket
|
||||
@@ -107,15 +111,60 @@ func (s *System) Attributes(fileKey string) (*blob.Attributes, error) {
|
||||
return s.bucket.Attributes(s.ctx, fileKey)
|
||||
}
|
||||
|
||||
// GetFile returns a file content reader for the given fileKey.
|
||||
// GetReader returns a file content reader for the given fileKey.
|
||||
//
|
||||
// NB! Make sure to call Close() on the file after you are done working with it.
|
||||
//
|
||||
// If the file doesn't exist returns ErrNotFound.
|
||||
func (s *System) GetFile(fileKey string) (*blob.Reader, error) {
|
||||
func (s *System) GetReader(fileKey string) (*blob.Reader, error) {
|
||||
return s.bucket.NewReader(s.ctx, fileKey)
|
||||
}
|
||||
|
||||
// Deprecated: Please use GetReader(fileKey) instead.
|
||||
func (s *System) GetFile(fileKey string) (*blob.Reader, error) {
|
||||
color.Yellow("Deprecated: Please replace GetFile with GetReader.")
|
||||
return s.GetReader(fileKey)
|
||||
}
|
||||
|
||||
// GetReuploadableFile constructs a new reuploadable File value
|
||||
// from the associated fileKey blob.Reader.
|
||||
//
|
||||
// If preserveName is false then the returned File.Name will have
|
||||
// a new randomly generated suffix, otherwise it will reuse the original one.
|
||||
//
|
||||
// This method could be useful in case you want to clone an existing
|
||||
// Record file and assign it to a new Record (e.g. in a Record duplicate action).
|
||||
//
|
||||
// If you simply want to copy an existing file to a new location you
|
||||
// could check the Copy(srcKey, dstKey) method.
|
||||
func (s *System) GetReuploadableFile(fileKey string, preserveName bool) (*File, error) {
|
||||
attrs, err := s.Attributes(fileKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name := path.Base(fileKey)
|
||||
originalName := attrs.Metadata[metadataOriginalName]
|
||||
if originalName == "" {
|
||||
originalName = name
|
||||
}
|
||||
|
||||
file := &File{}
|
||||
file.Size = attrs.Size
|
||||
file.OriginalName = originalName
|
||||
file.Reader = openFuncAsReader(func() (io.ReadSeekCloser, error) {
|
||||
return s.GetReader(fileKey)
|
||||
})
|
||||
|
||||
if preserveName {
|
||||
file.Name = name
|
||||
} else {
|
||||
file.Name = normalizeName(file.Reader, originalName)
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// Copy copies the file stored at srcKey to dstKey.
|
||||
//
|
||||
// If srcKey file doesn't exist, it returns ErrNotFound.
|
||||
@@ -190,7 +239,7 @@ func (s *System) UploadFile(file *File, fileKey string) error {
|
||||
opts := &blob.WriterOptions{
|
||||
ContentType: mt.String(),
|
||||
Metadata: map[string]string{
|
||||
"original-filename": originalName,
|
||||
metadataOriginalName: originalName,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -232,7 +281,7 @@ func (s *System) UploadMultipart(fh *multipart.FileHeader, fileKey string) error
|
||||
opts := &blob.WriterOptions{
|
||||
ContentType: mt.String(),
|
||||
Metadata: map[string]string{
|
||||
"original-filename": originalName,
|
||||
metadataOriginalName: originalName,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -241,7 +290,8 @@ func (s *System) UploadMultipart(fh *multipart.FileHeader, fileKey string) error
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := w.ReadFrom(f); err != nil {
|
||||
_, err = w.ReadFrom(f)
|
||||
if err != nil {
|
||||
w.Close()
|
||||
return err
|
||||
}
|
||||
@@ -379,7 +429,7 @@ const forceAttachmentParam = "download"
|
||||
// Internally this method uses [http.ServeContent] so Range requests,
|
||||
// If-Match, If-Unmodified-Since, etc. headers are handled transparently.
|
||||
func (s *System) Serve(res http.ResponseWriter, req *http.Request, fileKey string, name string) error {
|
||||
br, readErr := s.GetFile(fileKey)
|
||||
br, readErr := s.GetReader(fileKey)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
@@ -451,7 +501,7 @@ func (s *System) CreateThumb(originalKey string, thumbKey, thumbSize string) err
|
||||
}
|
||||
|
||||
// fetch the original
|
||||
r, readErr := s.GetFile(originalKey)
|
||||
r, readErr := s.GetReader(originalKey)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
|
||||
@@ -526,7 +526,7 @@ func TestFileSystemServe(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSystemGetFile(t *testing.T) {
|
||||
func TestFileSystemGetReader(t *testing.T) {
|
||||
dir := createTestDir(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
@@ -547,7 +547,7 @@ func TestFileSystemGetFile(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.file, func(t *testing.T) {
|
||||
f, err := fsys.GetFile(s.file)
|
||||
f, err := fsys.GetReader(s.file)
|
||||
defer func() {
|
||||
if f != nil {
|
||||
f.Close()
|
||||
@@ -578,6 +578,83 @@ func TestFileSystemGetFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSystemGetReuploadableFile(t *testing.T) {
|
||||
dir := createTestDir(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
fsys, err := filesystem.NewLocal(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer fsys.Close()
|
||||
|
||||
t.Run("missing.txt", func(t *testing.T) {
|
||||
_, err := fsys.GetReuploadableFile("missing.txt", false)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
testReader := func(t *testing.T, f *filesystem.File, expectedContent string) {
|
||||
r, err := f.Reader.Open()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rawStr := string(raw)
|
||||
|
||||
if rawStr != expectedContent {
|
||||
t.Fatalf("Expected content %q, got %q", expectedContent, rawStr)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("existing (preserve name)", func(t *testing.T) {
|
||||
file, err := fsys.GetReuploadableFile("test/sub1.txt", true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if v := file.OriginalName; v != "sub1.txt" {
|
||||
t.Fatalf("Expected originalName %q, got %q", "sub1.txt", v)
|
||||
}
|
||||
|
||||
if v := file.Size; v != 4 {
|
||||
t.Fatalf("Expected size %d, got %d", 4, v)
|
||||
}
|
||||
|
||||
if v := file.Name; v != "sub1.txt" {
|
||||
t.Fatalf("Expected name to be preserved, got %q", v)
|
||||
}
|
||||
|
||||
testReader(t, file, "sub1")
|
||||
})
|
||||
|
||||
t.Run("existing (new random suffix name)", func(t *testing.T) {
|
||||
file, err := fsys.GetReuploadableFile("test/sub1.txt", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if v := file.OriginalName; v != "sub1.txt" {
|
||||
t.Fatalf("Expected originalName %q, got %q", "sub1.txt", v)
|
||||
}
|
||||
|
||||
if v := file.Size; v != 4 {
|
||||
t.Fatalf("Expected size %d, got %d", 4, v)
|
||||
}
|
||||
|
||||
if v := file.Name; v == "sub1.txt" || len(v) <= len("sub1.txt.png") {
|
||||
t.Fatalf("Expected name to have new random suffix, got %q", v)
|
||||
}
|
||||
|
||||
testReader(t, file, "sub1")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileSystemCopy(t *testing.T) {
|
||||
dir := createTestDir(t)
|
||||
defer os.RemoveAll(dir)
|
||||
@@ -600,7 +677,7 @@ func TestFileSystemCopy(t *testing.T) {
|
||||
if err := fsys.Copy(src, dst); err != nil {
|
||||
t.Fatalf("Failed to copy %q to %q: %v", src, dst, err)
|
||||
}
|
||||
f, err := fsys.GetFile(dst)
|
||||
f, err := fsys.GetReader(dst)
|
||||
//nolint
|
||||
defer f.Close()
|
||||
if err != nil {
|
||||
@@ -801,7 +878,7 @@ func TestFileSystemCreateThumb(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
f, err := fsys.GetFile(s.thumb)
|
||||
f, err := fsys.GetReader(s.thumb)
|
||||
if err != nil {
|
||||
t.Fatalf("Missing expected thumb %s (%v)", s.thumb, err)
|
||||
}
|
||||
@@ -849,7 +926,6 @@ func createTestDir(t *testing.T) string {
|
||||
|
||||
// png
|
||||
{
|
||||
|
||||
file, err := os.OpenFile(filepath.Join(dir, "image.png"), os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -200,7 +200,9 @@ func (drv *driver) NewRangeReader(ctx context.Context, key string, offset, lengt
|
||||
}
|
||||
|
||||
reqOpt := func(req *http.Request) {
|
||||
req.Header.Set("Range", byteRange)
|
||||
if byteRange != "" {
|
||||
req.Header.Set("Range", byteRange)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := drv.s3.GetObject(ctx, key, reqOpt)
|
||||
|
||||
@@ -296,6 +296,10 @@ func setRegularReflectedValue(rv reflect.Value, value string) error {
|
||||
}
|
||||
|
||||
rv.SetFloat(v)
|
||||
case reflect.Interface:
|
||||
if rv.CanSet() {
|
||||
rv.Set(reflect.ValueOf(inferValue(value)))
|
||||
}
|
||||
default:
|
||||
return errors.New("unknown value type " + rv.Kind().String())
|
||||
}
|
||||
|
||||
@@ -51,6 +51,12 @@ func TestUnmarshalRequestData(t *testing.T) {
|
||||
}
|
||||
|
||||
structData := map[string][]string{
|
||||
"anyTag": {"a", "b"},
|
||||
"AnyPtr": {"b"},
|
||||
"AnySlice": {"a", "b", "false", ""},
|
||||
"anySlicePtrTag": {"d", "true"},
|
||||
"AnySliceOfPtr": {"f", "123.456"},
|
||||
|
||||
"stringTag": {"a", "b"},
|
||||
"StringPtr": {"b"},
|
||||
"StringSlice": {"a", "b", "c", ""},
|
||||
@@ -172,6 +178,12 @@ func TestUnmarshalRequestData(t *testing.T) {
|
||||
|
||||
//nolint
|
||||
type TestStruct struct {
|
||||
Any any `form:"anyTag" query:"anyTag2"`
|
||||
AnyPtr *any
|
||||
AnySlice []any
|
||||
AnySlicePtr *[]any `form:"anySlicePtrTag"`
|
||||
AnySliceOfPtr []*any
|
||||
|
||||
String string `form:"stringTag" query:"stringTag2"`
|
||||
StringPtr *string
|
||||
StringSlice []string
|
||||
@@ -335,7 +347,7 @@ func TestUnmarshalRequestData(t *testing.T) {
|
||||
name: "valid pointer struct (all fields)",
|
||||
data: structData,
|
||||
dst: &TestStruct{},
|
||||
result: `{"String":"a","StringPtr":"b","StringSlice":["a","b","c",""],"StringSlicePtr":["d","e"],"StringSliceOfPtr":["f","g"],"Bool":true,"BoolPtr":true,"BoolSlice":[true,false,false],"BoolSlicePtr":[false,false,true],"BoolSliceOfPtr":[false,true,false],"Int8":-1,"Int8Ptr":3,"Int8Slice":[4,5,0],"Int8SlicePtr":[5,6],"Int8SliceOfPtr":[7,8],"Int16":-1,"Int16Ptr":3,"Int16Slice":[4,5,0],"Int16SlicePtr":[5,6],"Int16SliceOfPtr":[7,8],"Int32":-1,"Int32Ptr":3,"Int32Slice":[4,5,0],"Int32SlicePtr":[5,6],"Int32SliceOfPtr":[7,8],"Int64":-1,"Int64Ptr":3,"Int64Slice":[4,5,0],"Int64SlicePtr":[5,6],"Int64SliceOfPtr":[7,8],"Int":-1,"IntPtr":3,"IntSlice":[4,5,0],"IntSlicePtr":[5,6],"IntSliceOfPtr":[7,8],"Uint8":1,"Uint8Ptr":3,"Uint8Slice":"BAUA","Uint8SlicePtr":"BQY=","Uint8SliceOfPtr":[7,8],"Uint16":1,"Uint16Ptr":3,"Uint16Slice":[4,5,0],"Uint16SlicePtr":[5,6],"Uint16SliceOfPtr":[7,8],"Uint32":1,"Uint32Ptr":3,"Uint32Slice":[4,5,0],"Uint32SlicePtr":[5,6],"Uint32SliceOfPtr":[7,8],"Uint64":1,"Uint64Ptr":3,"Uint64Slice":[4,5,0],"Uint64SlicePtr":[5,6],"Uint64SliceOfPtr":[7,8],"Uint":1,"UintPtr":3,"UintSlice":[4,5,0],"UintSlicePtr":[5,6],"UintSliceOfPtr":[7,8],"Float32":-1.2,"Float32Ptr":1.5,"Float32Slice":[1,2.3,-0.3,0],"Float32SlicePtr":[-1.3,3],"Float32SliceOfPtr":[0,1.2],"Float64":-1.2,"Float64Ptr":1.5,"Float64Slice":[1,2.3,-0.3,0],"Float64SlicePtr":[-1.3,3],"Float64SliceOfPtr":[0,1.2],"Time":"2009-11-10T15:00:00Z","TimePtr":"2009-11-10T14:00:00Z","TimeSlice":["2009-11-10T14:00:00Z","2009-11-10T15:00:00Z"],"TimeSlicePtr":["2009-11-10T15:00:00Z","2009-11-10T16:00:00Z"],"TimeSliceOfPtr":["2009-11-10T17:00:00Z","2009-11-10T18:00:00Z"],"payloadA":"test","payloadB":[1,2,3],"SkipExported":"","StructWithoutTag":{"StructWithoutTag_name":"test1"},"StructWithTag":{"StructWithTag_name":"test2"},"embed_name":"test3","embed_name2":"test4"}`,
|
||||
result: `{"Any":"a","AnyPtr":"b","AnySlice":["a","b",false,""],"AnySlicePtr":["d",true],"AnySliceOfPtr":["f",123.456],"String":"a","StringPtr":"b","StringSlice":["a","b","c",""],"StringSlicePtr":["d","e"],"StringSliceOfPtr":["f","g"],"Bool":true,"BoolPtr":true,"BoolSlice":[true,false,false],"BoolSlicePtr":[false,false,true],"BoolSliceOfPtr":[false,true,false],"Int8":-1,"Int8Ptr":3,"Int8Slice":[4,5,0],"Int8SlicePtr":[5,6],"Int8SliceOfPtr":[7,8],"Int16":-1,"Int16Ptr":3,"Int16Slice":[4,5,0],"Int16SlicePtr":[5,6],"Int16SliceOfPtr":[7,8],"Int32":-1,"Int32Ptr":3,"Int32Slice":[4,5,0],"Int32SlicePtr":[5,6],"Int32SliceOfPtr":[7,8],"Int64":-1,"Int64Ptr":3,"Int64Slice":[4,5,0],"Int64SlicePtr":[5,6],"Int64SliceOfPtr":[7,8],"Int":-1,"IntPtr":3,"IntSlice":[4,5,0],"IntSlicePtr":[5,6],"IntSliceOfPtr":[7,8],"Uint8":1,"Uint8Ptr":3,"Uint8Slice":"BAUA","Uint8SlicePtr":"BQY=","Uint8SliceOfPtr":[7,8],"Uint16":1,"Uint16Ptr":3,"Uint16Slice":[4,5,0],"Uint16SlicePtr":[5,6],"Uint16SliceOfPtr":[7,8],"Uint32":1,"Uint32Ptr":3,"Uint32Slice":[4,5,0],"Uint32SlicePtr":[5,6],"Uint32SliceOfPtr":[7,8],"Uint64":1,"Uint64Ptr":3,"Uint64Slice":[4,5,0],"Uint64SlicePtr":[5,6],"Uint64SliceOfPtr":[7,8],"Uint":1,"UintPtr":3,"UintSlice":[4,5,0],"UintSlicePtr":[5,6],"UintSliceOfPtr":[7,8],"Float32":-1.2,"Float32Ptr":1.5,"Float32Slice":[1,2.3,-0.3,0],"Float32SlicePtr":[-1.3,3],"Float32SliceOfPtr":[0,1.2],"Float64":-1.2,"Float64Ptr":1.5,"Float64Slice":[1,2.3,-0.3,0],"Float64SlicePtr":[-1.3,3],"Float64SliceOfPtr":[0,1.2],"Time":"2009-11-10T15:00:00Z","TimePtr":"2009-11-10T14:00:00Z","TimeSlice":["2009-11-10T14:00:00Z","2009-11-10T15:00:00Z"],"TimeSlicePtr":["2009-11-10T15:00:00Z","2009-11-10T16:00:00Z"],"TimeSliceOfPtr":["2009-11-10T17:00:00Z","2009-11-10T18:00:00Z"],"payloadA":"test","payloadB":[1,2,3],"SkipExported":"","StructWithoutTag":{"StructWithoutTag_name":"test1"},"StructWithTag":{"StructWithTag_name":"test2"},"embed_name":"test3","embed_name2":"test4"}`,
|
||||
},
|
||||
{
|
||||
name: "non-pointer struct",
|
||||
|
||||
@@ -2,6 +2,7 @@ package security
|
||||
|
||||
import (
|
||||
cryptoRand "crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"regexp/syntax"
|
||||
@@ -128,21 +129,47 @@ func repeatRandomStringByRegex(r *syntax.Regexp, sb *strings.Builder, min int, m
|
||||
}
|
||||
|
||||
func randomRuneFromPairs(pairs []rune) (rune, error) {
|
||||
idx, err := randomNumber(len(pairs) / 2)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
if len(pairs)%2 != 0 {
|
||||
return 0, fmt.Errorf("invalid pairs slice: odd number of elements")
|
||||
}
|
||||
|
||||
return randomRuneFromRange(pairs[idx*2], pairs[idx*2+1])
|
||||
}
|
||||
|
||||
func randomRuneFromRange(min rune, max rune) (rune, error) {
|
||||
offset, err := randomNumber(int(max - min + 1))
|
||||
if err != nil {
|
||||
return min, err
|
||||
// Pre-calculate the cumulative size of all ranges to make the selection process more efficient.
|
||||
cumulativeSizes := make([]int, len(pairs)/2)
|
||||
totalRunes := 0
|
||||
for i := 0; i < len(pairs); i += 2 {
|
||||
start, end := pairs[i], pairs[i+1]
|
||||
if start > end {
|
||||
return 0, fmt.Errorf("invalid range: start '%c' > end '%c'", start, end)
|
||||
}
|
||||
totalRunes += int(end - start + 1)
|
||||
cumulativeSizes[i/2] = totalRunes
|
||||
}
|
||||
|
||||
return min + rune(offset), nil
|
||||
if totalRunes == 0 {
|
||||
return 0, errors.New("no runes to choose from")
|
||||
}
|
||||
|
||||
// Select a random number in the range of total runes.
|
||||
runeNumber, err := randomNumber(totalRunes)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to generate random number: %w", err)
|
||||
}
|
||||
|
||||
// Find which range the selected number falls into using the pre-calculated cumulative sizes.
|
||||
for i, size := range cumulativeSizes {
|
||||
if runeNumber < size {
|
||||
startRune := pairs[i*2]
|
||||
previousSize := 0
|
||||
if i > 0 {
|
||||
previousSize = cumulativeSizes[i-1]
|
||||
}
|
||||
return startRune + rune(runeNumber-previousSize), nil
|
||||
}
|
||||
}
|
||||
|
||||
// This part should be unreachable if the logic is correct.
|
||||
// It indicates a bug in this function or in randomNumber.
|
||||
panic("unreachable: failed to find a rune")
|
||||
}
|
||||
|
||||
func randomNumber(maxSoft int) (int, error) {
|
||||
|
||||
@@ -26,10 +26,9 @@ func TestRandomStringByRegex(t *testing.T) {
|
||||
{`\d{1,10}`, nil, false},
|
||||
{`\d{3}`, nil, false},
|
||||
{`\d{0,}-abc`, nil, false},
|
||||
{`[a-zA-Z]*`, nil, false},
|
||||
{`[a-zA-Z_]*`, nil, false},
|
||||
{`[^a-zA-Z]{5,30}`, nil, false},
|
||||
{`\w+_abc`, nil, false},
|
||||
{`[a-zA-Z_]*`, nil, false},
|
||||
{`[2-9]{5}-\w+`, nil, false},
|
||||
{`(a|b|c)`, nil, false},
|
||||
}
|
||||
|
||||
@@ -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.27.2"
|
||||
PB_VERSION = "v0.29.2"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as Ce,i as Be,s as Te,V as Le,X as J,h as u,d as ae,t as Q,a as G,I as N,Z as we,_ as Se,C as De,$ as Re,D as Ue,l as d,n as a,m as ne,u as c,A as y,v as k,c as ie,w as h,p as oe,J as je,k as O,o as qe,W as Ee}from"./index-rRnVk674.js";import{F as Fe}from"./FieldsQueryParam-BEgqkb0I.js";function ye(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=y(o),b=k(),h(l,"class","tab-item"),O(l,"active",s[1]===s[8].code),this.first=l},m(v,$){d(v,l,$),a(l,p),a(l,b),i||(f=qe(l,"click",m),i=!0)},p(v,$){s=v,$&4&&o!==(o=s[8].code+"")&&N(p,o),$&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 Ee({props:{content:s[8].body}}),{key:n,first:null,c(){l=c("div"),ie(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||(G(o.$$.fragment,i),b=!0)},o(i){Q(o.$$.fragment,i),b=!1},d(i){i&&u(l),ae(o)}}}function He(n){var ke,ge;let s,l,o=n[0].name+"",p,b,i,f,m,v,$,g=n[0].name+"",V,ce,W,M,X,L,Z,A,E,re,F,S,ue,z,H=n[0].name+"",K,de,Y,D,x,P,ee,fe,te,T,le,R,se,C,U,w=[],me=new Map,pe,j,_=[],be=new Map,B;M=new Le({props:{js:`
|
||||
import{S as Ce,i as Be,s as Te,V as Le,X as J,h as u,d as ae,t as Q,a as G,I as N,Z as we,_ as Se,C as De,$ as Re,D as Ue,l as d,n as a,m as ne,u as c,A as y,v as k,c as ie,w as h,p as oe,J as je,k as O,o as qe,W as Ee}from"./index-CAtANE99.js";import{F as Fe}from"./FieldsQueryParam-IbHyEItj.js";function ye(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=y(o),b=k(),h(l,"class","tab-item"),O(l,"active",s[1]===s[8].code),this.first=l},m(v,$){d(v,l,$),a(l,p),a(l,b),i||(f=qe(l,"click",m),i=!0)},p(v,$){s=v,$&4&&o!==(o=s[8].code+"")&&N(p,o),$&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 Ee({props:{content:s[8].body}}),{key:n,first:null,c(){l=c("div"),ie(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||(G(o.$$.fragment,i),b=!0)},o(i){Q(o.$$.fragment,i),b=!1},d(i){i&&u(l),ae(o)}}}function He(n){var ke,ge;let s,l,o=n[0].name+"",p,b,i,f,m,v,$,g=n[0].name+"",V,ce,W,M,X,L,Z,A,E,re,F,S,ue,z,H=n[0].name+"",K,de,Y,D,x,P,ee,fe,te,T,le,R,se,C,U,w=[],me=new Map,pe,j,_=[],be=new Map,B;M=new Le({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${n[3]}');
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as je,i as xe,s as Ie,V as Ke,W as Ue,X as I,h as d,d as K,t as E,a as z,I as de,Z as Oe,_ as Qe,C as We,$ as Xe,D as Ze,l as u,n as o,m as Q,u as s,A as k,v as p,c as W,w as b,J as Ve,p as Ge,k as X,o as Ye}from"./index-rRnVk674.js";import{F as et}from"./FieldsQueryParam-BEgqkb0I.js";function Ee(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"),X(l,"active",a[1]===a[5].code),this.first=l},m(v,w){u(v,l,w),o(l,m),o(l,_),i||(h=Ye(l,"click",g),i=!0)},p(v,w){a=v,w&4&&n!==(n=a[5].code+"")&&de(m,n),w&6&&X(l,"active",a[1]===a[5].code)},d(v){v&&d(l),i=!1,h()}}}function Ne(r,a){let l,n,m,_;return n=new Ue({props:{content:a[5].body}}),{key:r,first:null,c(){l=s("div"),W(n.$$.fragment),m=p(),b(l,"class","tab-item"),X(l,"active",a[1]===a[5].code),this.first=l},m(i,h){u(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)&&X(l,"active",a[1]===a[5].code)},i(i){_||(z(n.$$.fragment,i),_=!0)},o(i){E(n.$$.fragment,i),_=!1},d(i){i&&d(l),K(n)}}}function tt(r){var qe,Fe;let a,l,n=r[0].name+"",m,_,i,h,g,v,w,D,Z,S,J,ue,N,M,pe,G,U=r[0].name+"",Y,he,fe,j,ee,q,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,A,ie,H,ce,R,L,y=[],Pe=new Map,Ae,O,$=[],Be=new Map,B;v=new Ke({props:{js:`
|
||||
import{S as je,i as xe,s as Ie,V as Ke,W as Ue,X as I,h as d,d as K,t as E,a as z,I as de,Z as Oe,_ as Qe,C as We,$ as Xe,D as Ze,l as u,n as o,m as Q,u as s,A as k,v as p,c as W,w as b,J as Ve,p as Ge,k as X,o as Ye}from"./index-CAtANE99.js";import{F as et}from"./FieldsQueryParam-IbHyEItj.js";function Ee(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"),X(l,"active",a[1]===a[5].code),this.first=l},m(v,w){u(v,l,w),o(l,m),o(l,_),i||(h=Ye(l,"click",g),i=!0)},p(v,w){a=v,w&4&&n!==(n=a[5].code+"")&&de(m,n),w&6&&X(l,"active",a[1]===a[5].code)},d(v){v&&d(l),i=!1,h()}}}function Ne(r,a){let l,n,m,_;return n=new Ue({props:{content:a[5].body}}),{key:r,first:null,c(){l=s("div"),W(n.$$.fragment),m=p(),b(l,"class","tab-item"),X(l,"active",a[1]===a[5].code),this.first=l},m(i,h){u(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)&&X(l,"active",a[1]===a[5].code)},i(i){_||(z(n.$$.fragment,i),_=!0)},o(i){E(n.$$.fragment,i),_=!1},d(i){i&&d(l),K(n)}}}function tt(r){var qe,Fe;let a,l,n=r[0].name+"",m,_,i,h,g,v,w,D,Z,S,J,ue,N,M,pe,G,U=r[0].name+"",Y,he,fe,j,ee,q,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,A,ie,H,ce,R,L,y=[],Pe=new Map,Ae,O,$=[],Be=new Map,B;v=new Ke({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${r[3]}');
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as Je,i as xe,s as Ee,V as Ne,W as je,X as Q,h as r,d as Z,t as j,a as J,I as pe,Z as Ue,_ as Ie,C as Qe,$ as Ze,D as ze,l as c,n as a,m as z,u as o,A as _,v as h,c as K,w as p,J as Be,p as Ke,k as X,o as Xe}from"./index-rRnVk674.js";import{F as Ge}from"./FieldsQueryParam-BEgqkb0I.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 He(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){c(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&&r(n),d=!1,b()}}}function Ve(s,l){let n,i,f,g;return i=new je({props:{content:l[5].body}}),{key:s,first:null,c(){n=o("div"),K(i.$$.fragment),f=h(),p(n,"class","tab-item"),X(n,"active",l[1]===l[5].code),this.first=n},m(d,b){c(d,n,b),z(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||(J(i.$$.fragment,d),g=!0)},o(d){j(i.$$.fragment,d),g=!1},d(d){d&&r(n),Z(i)}}}function Ye(s){let l,n,i=s[0].name+"",f,g,d,b,k,v,O,R,G,A,x,be,E,P,me,Y,N=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,L,$=[],De=new Map,Re,H,w=[],Pe=new Map,D;v=new Ne({props:{js:`
|
||||
import{S as Je,i as xe,s as Ee,V as Ne,W as je,X as Q,h as r,d as Z,t as j,a as J,I as pe,Z as Ue,_ as Ie,C as Qe,$ as Ze,D as ze,l as c,n as a,m as z,u as o,A as _,v as h,c as K,w as p,J as Be,p as Ke,k as X,o as Xe}from"./index-CAtANE99.js";import{F as Ge}from"./FieldsQueryParam-IbHyEItj.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 He(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){c(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&&r(n),d=!1,b()}}}function Ve(s,l){let n,i,f,g;return i=new je({props:{content:l[5].body}}),{key:s,first:null,c(){n=o("div"),K(i.$$.fragment),f=h(),p(n,"class","tab-item"),X(n,"active",l[1]===l[5].code),this.first=n},m(d,b){c(d,n,b),z(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||(J(i.$$.fragment,d),g=!0)},o(d){j(i.$$.fragment,d),g=!1},d(d){d&&r(n),Z(i)}}}function Ye(s){let l,n,i=s[0].name+"",f,g,d,b,k,v,O,R,G,A,x,be,E,P,me,Y,N=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,L,$=[],De=new Map,Re,H,w=[],Pe=new Map,D;v=new Ne({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${s[3]}');
|
||||
Loaded 100 of 190 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user