mirror of
https://github.com/pocketbase/pocketbase.git
synced 2026-10-02 04:21:44 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff1f99436a | ||
|
|
3ea02c945d | ||
|
|
4c903684d8 | ||
|
|
d0239f25ed | ||
|
|
05adb8c018 | ||
|
|
c901c9ab7d | ||
|
|
c9fba9972a | ||
|
|
2c40811fac | ||
|
|
b81112f82e | ||
|
|
0acf8198a4 | ||
|
|
5ece360808 | ||
|
|
3a5d3d521f | ||
|
|
f024de3cc4 | ||
|
|
a4abe9e2cb | ||
|
|
5678339af0 | ||
|
|
e5a22b8bd8 | ||
|
|
be0ee7f66c | ||
|
|
78a9f7429c | ||
|
|
d8b4daf47f | ||
|
|
7a2360d785 | ||
|
|
67ecebe935 | ||
|
|
5cbdbf6c10 | ||
|
|
0710214701 | ||
|
|
9736a45e80 | ||
|
|
923fc26a31 | ||
|
|
a79f3a7c56 | ||
|
|
8430944650 | ||
|
|
9b54fd3516 | ||
|
|
226352f884 | ||
|
|
2a682a10b2 | ||
|
|
b67549209a | ||
|
|
1126a0e16f | ||
|
|
17472cb40a | ||
|
|
981de64c7f | ||
|
|
1b45e23c81 | ||
|
|
29621303df | ||
|
|
a0ec5707d1 | ||
|
|
695c20a969 | ||
|
|
95bb2eb871 | ||
|
|
971916c20d | ||
|
|
44f5172db7 | ||
|
|
5fd103481c | ||
|
|
82abd7e0b0 | ||
|
|
361cfb56b7 | ||
|
|
4d16d0e16e | ||
|
|
254e691e92 | ||
|
|
e735d9d21b | ||
|
|
31b24ecb86 | ||
|
|
3eb3d0bbdf | ||
|
|
f180af7af7 | ||
|
|
d046811df7 | ||
|
|
4768e07c0b | ||
|
|
0547202382 | ||
|
|
684f91f7e5 | ||
|
|
bce4094134 | ||
|
|
6a60bc1df3 | ||
|
|
19b7a7a74f | ||
|
|
8de0e61cf4 | ||
|
|
5344ec83fa | ||
|
|
65aa114103 | ||
|
|
8728161288 | ||
|
|
2420b2804a | ||
|
|
2791628d96 | ||
|
|
01f0af7af6 | ||
|
|
e8d61e7b45 | ||
|
|
a5ac83c7b0 | ||
|
|
a67c14c368 | ||
|
|
19faa0d8e7 | ||
|
|
56a77fd5ff | ||
|
|
07727dbde6 | ||
|
|
a3d26a73c3 | ||
|
|
b328827705 | ||
|
|
f5e5fae773 | ||
|
|
41f01bab0d |
No files matched your search
@@ -42,6 +42,6 @@ jobs:
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
+25
-2
@@ -7,7 +7,20 @@ before:
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- main: ./examples/base
|
||||
- id: build_cgo
|
||||
main: ./examples/base
|
||||
binary: pocketbase
|
||||
ldflags:
|
||||
- -s -w -X github.com/pocketbase/pocketbase.Version={{ .Version }}
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
|
||||
- id: build_noncgo
|
||||
main: ./examples/base
|
||||
binary: pocketbase
|
||||
ldflags:
|
||||
- -s -w -X github.com/pocketbase/pocketbase.Version={{ .Version }}
|
||||
@@ -33,7 +46,17 @@ release:
|
||||
draft: true
|
||||
|
||||
archives:
|
||||
- format: zip
|
||||
- id: archive_cgo
|
||||
builds: [build_cgo]
|
||||
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}_cgo'
|
||||
format: zip
|
||||
files:
|
||||
- LICENSE*
|
||||
- CHANGELOG*
|
||||
|
||||
- id: archive_noncgo
|
||||
builds: [build_noncgo]
|
||||
format: zip
|
||||
files:
|
||||
- LICENSE*
|
||||
- CHANGELOG*
|
||||
|
||||
@@ -1,3 +1,63 @@
|
||||
## v0.14.0
|
||||
|
||||
- Added _experimental_ Apple OAuth2 integration.
|
||||
|
||||
- Added `@request.headers.*` filter rule support.
|
||||
|
||||
- Added support for advanced unique constraints and indexes management ([#345](https://github.com/pocketbase/pocketbase/issues/345), [#544](https://github.com/pocketbase/pocketbase/issues/544))
|
||||
|
||||
- Simplified the collections fields UI to allow easier and quicker scaffolding of the data schema.
|
||||
|
||||
- Deprecated `SchemaField.Unique`. Unique constraints are now managed via indexes.
|
||||
The `Unique` field is a no-op and will be removed in future version.
|
||||
|
||||
- Removed the `COALESCE` wrapping from some of the generated filter conditions to make better use of the indexes ([#1939](https://github.com/pocketbase/pocketbase/issues/1939)).
|
||||
|
||||
- Detect `id` aliased view columns as single `relation` fields ([#2029](https://github.com/pocketbase/pocketbase/discussions/2029)).
|
||||
|
||||
- Optimized single relation lookups.
|
||||
|
||||
- Normalized record values on `maxSelect` field option change (`select`, `file`, `relation`).
|
||||
When changing **from single to multiple** all already inserted single values are converted to an array.
|
||||
When changing **from multiple to single** only the last item of the already inserted array items is kept.
|
||||
|
||||
- Changed the cost/round factor of bcrypt hash generation from 13 to 12 since several users complained about the slow authWithPassword responses on lower spec hardware.
|
||||
_The change will affect only new users. Depending on the demand, we might make it configurable from the auth options._
|
||||
|
||||
- Simplified the default mail template styles to allow more control over the template layout ([#1904](https://github.com/pocketbase/pocketbase/issues/1904)).
|
||||
|
||||
- Added option to explicitly set the record id from the Admin UI ([#2118](https://github.com/pocketbase/pocketbase/issues/2118)).
|
||||
|
||||
- Added `migrate history-sync` command to clean `_migrations` history table from deleted migration files references.
|
||||
|
||||
- Added new fields to the `core.RecordAuthWithOAuth2Event` struct:
|
||||
```
|
||||
IsNewRecord bool, // boolean field indicating whether the OAuth2 action created a new auth record
|
||||
ProviderName string, // the name of the OAuth2 provider (eg. "google")
|
||||
ProviderClient auth.Provider, // the loaded Provider client instance
|
||||
```
|
||||
|
||||
- Added CGO linux target for the prebuilt executable.
|
||||
|
||||
- **!** Renamed `daos.GetTableColumns()` to `daos.TableColumns()` for consistency with the other Dao table related helpers.
|
||||
|
||||
- **!** Renamed `daos.GetTableInfo()` to `daos.TableInfo()` for consistency with the other Dao table related helpers.
|
||||
|
||||
- **!** Changed `types.JsonArray` to support specifying a generic type, aka. `types.JsonArray[T]`.
|
||||
If you have previously used `types.JsonArray`, you'll have to update it to `types.JsonArray[any]`.
|
||||
|
||||
- **!** Registered the `RemoveTrailingSlash` middleware only for the `/api/*` routes since it is causing issues with subpath file serving endpoints ([#2072](https://github.com/pocketbase/pocketbase/issues/2072)).
|
||||
|
||||
- **!** Changed the request logs `method` value to UPPERCASE, eg. "get" => "GET" ([#1956](https://github.com/pocketbase/pocketbase/discussions/1956)).
|
||||
|
||||
- Other minor UI improvements.
|
||||
|
||||
|
||||
## v0.13.4
|
||||
|
||||
- Removed eager unique collection name check to support lazy validation during bulk import.
|
||||
|
||||
|
||||
## v0.13.3
|
||||
|
||||
- Fixed view collections import ([#2044](https://github.com/pocketbase/pocketbase/issues/2044)).
|
||||
|
||||
+5
-2
@@ -36,8 +36,8 @@ func InitApi(app core.App) (*echo.Echo, error) {
|
||||
// default middlewares
|
||||
e.Pre(middleware.RemoveTrailingSlashWithConfig(middleware.RemoveTrailingSlashConfig{
|
||||
Skipper: func(c echo.Context) bool {
|
||||
// ignore Admin UI route(s)
|
||||
return strings.HasPrefix(c.Request().URL.Path, trailedAdminPath)
|
||||
// enable by default only for the API routes
|
||||
return !strings.HasPrefix(c.Request().URL.Path, "/api/")
|
||||
},
|
||||
}))
|
||||
e.Use(middleware.Recover())
|
||||
@@ -47,6 +47,9 @@ func InitApi(app core.App) (*echo.Echo, error) {
|
||||
// custom error handler
|
||||
e.HTTPErrorHandler = func(c echo.Context, err error) {
|
||||
if c.Response().Committed {
|
||||
if app.IsDebug() {
|
||||
log.Println("HTTPErrorHandler response was already committed:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -136,3 +136,76 @@ func TestCustomRoutesAndErrorsHandling(t *testing.T) {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveTrailingSlashMiddleware(t *testing.T) {
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
Name: "non /api/* route (exact match)",
|
||||
Method: http.MethodGet,
|
||||
Url: "/custom",
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/custom",
|
||||
Handler: func(c echo.Context) error {
|
||||
return c.String(200, "test123")
|
||||
},
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"test123"},
|
||||
},
|
||||
{
|
||||
Name: "non /api/* route (with trailing slash)",
|
||||
Method: http.MethodGet,
|
||||
Url: "/custom/",
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/custom",
|
||||
Handler: func(c echo.Context) error {
|
||||
return c.String(200, "test123")
|
||||
},
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 404,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "/api/* route (exact match)",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/custom",
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/custom",
|
||||
Handler: func(c echo.Context) error {
|
||||
return c.String(200, "test123")
|
||||
},
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"test123"},
|
||||
},
|
||||
{
|
||||
Name: "/api/* route (with trailing slash)",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/custom/",
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/custom",
|
||||
Handler: func(c echo.Context) error {
|
||||
return c.String(200, "test123")
|
||||
},
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"test123"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -159,7 +159,7 @@ func (api *collectionApi) delete(c echo.Context) error {
|
||||
|
||||
handlerErr := api.app.OnCollectionBeforeDeleteRequest().Trigger(event, func(e *core.CollectionDeleteEvent) error {
|
||||
if err := api.app.Dao().DeleteCollection(e.Collection); err != nil {
|
||||
return NewBadRequestError("Failed to delete collection. Make sure that the collection is not referenced by other collections.", err)
|
||||
return NewBadRequestError("Failed to delete collection due to existing dependency.", err)
|
||||
}
|
||||
|
||||
return e.HttpContext.NoContent(http.StatusNoContent)
|
||||
|
||||
+149
-1
@@ -586,6 +586,83 @@ func TestCollectionCreate(t *testing.T) {
|
||||
"OnCollectionAfterCreateRequest": 1,
|
||||
},
|
||||
},
|
||||
|
||||
// indexes
|
||||
// -----------------------------------------------------------
|
||||
{
|
||||
Name: "creating base collection with invalid indexes",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/collections",
|
||||
Body: strings.NewReader(`{
|
||||
"name":"new",
|
||||
"type":"base",
|
||||
"schema":[
|
||||
{"type":"text","name":"test"}
|
||||
],
|
||||
"indexes": [
|
||||
"create index idx_test1 on new (test)",
|
||||
"create index idx_test2 on new (missing)"
|
||||
]
|
||||
}`),
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{
|
||||
`"data":{`,
|
||||
`"indexes":{"1":{"code":"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnCollectionBeforeCreateRequest": 1,
|
||||
"OnModelBeforeCreate": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "creating base collection with valid indexes (+ random table name)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/collections",
|
||||
Body: strings.NewReader(`{
|
||||
"name":"new",
|
||||
"type":"base",
|
||||
"schema":[
|
||||
{"type":"text","name":"test"}
|
||||
],
|
||||
"indexes": [
|
||||
"create index idx_test1 on new (test)",
|
||||
"create index idx_test2 on anything (id, test)"
|
||||
]
|
||||
}`),
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"name":"new"`,
|
||||
`"type":"base"`,
|
||||
`"indexes":[`,
|
||||
`idx_test1`,
|
||||
`idx_test2`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnModelBeforeCreate": 1,
|
||||
"OnModelAfterCreate": 1,
|
||||
"OnCollectionBeforeCreateRequest": 1,
|
||||
"OnCollectionAfterCreateRequest": 1,
|
||||
},
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
indexes, err := app.Dao().TableIndexes("new")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := []string{"idx_test1", "idx_test2"}
|
||||
for name := range indexes {
|
||||
if !list.ExistInSlice(name, expected) {
|
||||
t.Fatalf("Missing index %q", name)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
@@ -951,6 +1028,68 @@ func TestCollectionUpdate(t *testing.T) {
|
||||
"OnCollectionAfterUpdateRequest": 1,
|
||||
},
|
||||
},
|
||||
|
||||
// indexes
|
||||
// -----------------------------------------------------------
|
||||
{
|
||||
Name: "updating base collection with invalid indexes",
|
||||
Method: http.MethodPatch,
|
||||
Url: "/api/collections/demo2",
|
||||
Body: strings.NewReader(`{
|
||||
"indexes": [
|
||||
"create unique idx_test1 on demo1 (text)",
|
||||
"create index idx_test2 on demo2 (id, title)"
|
||||
]
|
||||
}`),
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{
|
||||
`"data":{`,
|
||||
`"indexes":{"0":{"code":"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "updating base collection with valid indexes (+ random table name)",
|
||||
Method: http.MethodPatch,
|
||||
Url: "/api/collections/demo2",
|
||||
Body: strings.NewReader(`{
|
||||
"indexes": [
|
||||
"create unique index idx_test1 on demo2 (title)",
|
||||
"create index idx_test2 on anything (active)"
|
||||
]
|
||||
}`),
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"name":"demo2"`,
|
||||
`"indexes":[`,
|
||||
"CREATE UNIQUE INDEX `idx_test1`",
|
||||
"CREATE INDEX `idx_test2`",
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnModelBeforeUpdate": 1,
|
||||
"OnModelAfterUpdate": 1,
|
||||
"OnCollectionBeforeUpdateRequest": 1,
|
||||
"OnCollectionAfterUpdateRequest": 1,
|
||||
},
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
indexes, err := app.Dao().TableIndexes("new")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := []string{"idx_test1", "idx_test2"}
|
||||
for name := range indexes {
|
||||
if !list.ExistInSlice(name, expected) {
|
||||
t.Fatalf("Missing index %q", name)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
@@ -1018,7 +1157,7 @@ func TestCollectionImport(t *testing.T) {
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnCollectionsBeforeImportRequest": 1,
|
||||
"OnModelBeforeDelete": 6,
|
||||
"OnModelBeforeDelete": 7,
|
||||
},
|
||||
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
collections := []*models.Collection{}
|
||||
@@ -1097,6 +1236,9 @@ func TestCollectionImport(t *testing.T) {
|
||||
"name": "test",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"create index idx_test on import2 (test)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1120,10 +1262,16 @@ func TestCollectionImport(t *testing.T) {
|
||||
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := totalCollections + 3
|
||||
if len(collections) != expected {
|
||||
t.Fatalf("Expected %d collections, got %d", expected, len(collections))
|
||||
}
|
||||
|
||||
indexes, err := app.Dao().TableIndexes("import2")
|
||||
if err != nil || indexes["idx_test"] == "" {
|
||||
t.Fatalf("Missing index %s (%v)", "idx_test", err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -321,7 +321,7 @@ func ActivityLogger(app core.App) echo.MiddlewareFunc {
|
||||
|
||||
model := &models.Request{
|
||||
Url: httpRequest.URL.RequestURI(),
|
||||
Method: strings.ToLower(httpRequest.Method),
|
||||
Method: strings.ToUpper(httpRequest.Method),
|
||||
Status: status,
|
||||
Auth: requestAuth,
|
||||
UserIp: realUserIp(httpRequest, ip),
|
||||
|
||||
+20
-7
@@ -129,6 +129,16 @@ func (api *recordAuthApi) authMethods(c echo.Context) error {
|
||||
codeVerifier := security.RandomString(43)
|
||||
codeChallenge := security.S256Challenge(codeVerifier)
|
||||
codeChallengeMethod := "S256"
|
||||
urlOpts := []oauth2.AuthCodeOption{
|
||||
oauth2.SetAuthURLParam("code_challenge", codeChallenge),
|
||||
oauth2.SetAuthURLParam("code_challenge_method", codeChallengeMethod),
|
||||
}
|
||||
|
||||
if name == auth.NameApple {
|
||||
// see https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms#3332113
|
||||
urlOpts = append(urlOpts, oauth2.SetAuthURLParam("response_mode", "query"))
|
||||
}
|
||||
|
||||
result.AuthProviders = append(result.AuthProviders, providerInfo{
|
||||
Name: name,
|
||||
State: state,
|
||||
@@ -137,9 +147,8 @@ func (api *recordAuthApi) authMethods(c echo.Context) error {
|
||||
CodeChallengeMethod: codeChallengeMethod,
|
||||
AuthUrl: provider.BuildAuthUrl(
|
||||
state,
|
||||
oauth2.SetAuthURLParam("code_challenge", codeChallenge),
|
||||
oauth2.SetAuthURLParam("code_challenge_method", codeChallengeMethod),
|
||||
) + "&redirect_uri=", // empty redirect_uri so that users can append their url
|
||||
urlOpts...,
|
||||
) + "&redirect_uri=", // empty redirect_uri so that users can append their redirect url
|
||||
})
|
||||
}
|
||||
|
||||
@@ -168,10 +177,17 @@ func (api *recordAuthApi) authWithOAuth2(c echo.Context) error {
|
||||
return NewBadRequestError("An error occurred while loading the submitted data.", readErr)
|
||||
}
|
||||
|
||||
event := new(core.RecordAuthWithOAuth2Event)
|
||||
event.HttpContext = c
|
||||
event.Collection = collection
|
||||
event.ProviderName = form.Provider
|
||||
event.IsNewRecord = false
|
||||
|
||||
form.SetBeforeNewRecordCreateFunc(func(createForm *forms.RecordUpsert, authRecord *models.Record, authUser *auth.AuthUser) error {
|
||||
return createForm.DrySubmit(func(txDao *daos.Dao) error {
|
||||
requestData := RequestData(c)
|
||||
requestData.Data = form.CreateData
|
||||
event.IsNewRecord = true
|
||||
|
||||
createRuleFunc := func(q *dbx.SelectQuery) error {
|
||||
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
|
||||
@@ -204,14 +220,11 @@ func (api *recordAuthApi) authWithOAuth2(c echo.Context) error {
|
||||
})
|
||||
})
|
||||
|
||||
event := new(core.RecordAuthWithOAuth2Event)
|
||||
event.HttpContext = c
|
||||
event.Collection = collection
|
||||
|
||||
_, _, submitErr := form.Submit(func(next forms.InterceptorNextFunc[*forms.RecordOAuth2LoginData]) forms.InterceptorNextFunc[*forms.RecordOAuth2LoginData] {
|
||||
return func(data *forms.RecordOAuth2LoginData) error {
|
||||
event.Record = data.Record
|
||||
event.OAuth2User = data.OAuth2User
|
||||
event.ProviderClient = data.ProviderClient
|
||||
|
||||
return api.app.OnRecordBeforeAuthWithOAuth2Request().Trigger(event, func(e *core.RecordAuthWithOAuth2Event) error {
|
||||
data.Record = e.Record
|
||||
|
||||
+12
-3
@@ -30,9 +30,18 @@ func RequestData(c echo.Context) *models.RequestData {
|
||||
}
|
||||
|
||||
result := &models.RequestData{
|
||||
Method: c.Request().Method,
|
||||
Query: map[string]any{},
|
||||
Data: map[string]any{},
|
||||
Method: c.Request().Method,
|
||||
Query: map[string]any{},
|
||||
Data: map[string]any{},
|
||||
Headers: map[string]any{},
|
||||
}
|
||||
|
||||
// extract the first value of all headers and normalizes the keys
|
||||
// ("X-Token" is converted to "x_token")
|
||||
for k, v := range c.Request().Header {
|
||||
if len(v) > 0 {
|
||||
result.Headers[strings.ToLower(strings.ReplaceAll(k, "-", "_"))] = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
result.AuthRecord, _ = c.Get(ContextAuthRecordKey).(*models.Record)
|
||||
|
||||
@@ -17,6 +17,7 @@ func TestRequestData(t *testing.T) {
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodPost, "/?test=123", strings.NewReader(`{"test":456}`))
|
||||
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
||||
req.Header.Set("X-Token-Test", "123")
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
|
||||
@@ -38,6 +39,12 @@ func TestRequestData(t *testing.T) {
|
||||
t.Fatalf("Expected Method %v, got %v", http.MethodPost, result.Method)
|
||||
}
|
||||
|
||||
rawHeaders, _ := json.Marshal(result.Headers)
|
||||
expectedHeaders := `{"content_type":"application/json","x_token_test":"123"}`
|
||||
if v := string(rawHeaders); v != expectedHeaders {
|
||||
t.Fatalf("Expected Query %v, got %v", expectedHeaders, v)
|
||||
}
|
||||
|
||||
rawQuery, _ := json.Marshal(result.Query)
|
||||
expectedQuery := `{"test":"123"}`
|
||||
if v := string(rawQuery); v != expectedQuery {
|
||||
|
||||
+27
-1
@@ -22,6 +22,7 @@ func bindSettingsApi(app core.App, rg *echo.Group) {
|
||||
subGroup.PATCH("", api.set)
|
||||
subGroup.POST("/test/s3", api.testS3)
|
||||
subGroup.POST("/test/email", api.testEmail)
|
||||
subGroup.POST("/apple/generate-client-secret", api.generateAppleClientSecret)
|
||||
}
|
||||
|
||||
type settingsApi struct {
|
||||
@@ -121,8 +122,8 @@ func (api *settingsApi) testEmail(c echo.Context) error {
|
||||
|
||||
// send
|
||||
if err := form.Submit(); err != nil {
|
||||
// form error
|
||||
if fErr, ok := err.(validation.Errors); ok {
|
||||
// form error
|
||||
return NewBadRequestError("Failed to send the test email.", fErr)
|
||||
}
|
||||
|
||||
@@ -132,3 +133,28 @@ func (api *settingsApi) testEmail(c echo.Context) error {
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (api *settingsApi) generateAppleClientSecret(c echo.Context) error {
|
||||
form := forms.NewAppleClientSecretCreate(api.app)
|
||||
|
||||
// load request
|
||||
if err := c.Bind(form); err != nil {
|
||||
return NewBadRequestError("An error occurred while loading the submitted data.", err)
|
||||
}
|
||||
|
||||
// generate
|
||||
secret, err := form.Submit()
|
||||
if err != nil {
|
||||
// form error
|
||||
if fErr, ok := err.(validation.Errors); ok {
|
||||
return NewBadRequestError("Invalid client secret data.", fErr)
|
||||
}
|
||||
|
||||
// secret generation error
|
||||
return NewBadRequestError("Failed to generate client secret. Raw error: \n"+err.Error(), nil)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]any{
|
||||
"secret": secret,
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
package apis_test
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -65,6 +71,7 @@ func TestSettingsList(t *testing.T) {
|
||||
`"oidcAuth":{`,
|
||||
`"oidc2Auth":{`,
|
||||
`"oidc3Auth":{`,
|
||||
`"appleAuth":{`,
|
||||
`"secret":"******"`,
|
||||
`"clientSecret":"******"`,
|
||||
},
|
||||
@@ -139,6 +146,7 @@ func TestSettingsSet(t *testing.T) {
|
||||
`"oidcAuth":{`,
|
||||
`"oidc2Auth":{`,
|
||||
`"oidc3Auth":{`,
|
||||
`"appleAuth":{`,
|
||||
`"secret":"******"`,
|
||||
`"clientSecret":"******"`,
|
||||
`"appName":"acme_test"`,
|
||||
@@ -202,6 +210,7 @@ func TestSettingsSet(t *testing.T) {
|
||||
`"oidcAuth":{`,
|
||||
`"oidc2Auth":{`,
|
||||
`"oidc3Auth":{`,
|
||||
`"appleAuth":{`,
|
||||
`"secret":"******"`,
|
||||
`"clientSecret":"******"`,
|
||||
`"appName":"update_test"`,
|
||||
@@ -419,3 +428,116 @@ func TestSettingsTestEmail(t *testing.T) {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateAppleClientSecret(t *testing.T) {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
encodedKey, err := x509.MarshalPKCS8PrivateKey(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
privatePem := pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: encodedKey,
|
||||
},
|
||||
)
|
||||
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
Name: "unauthorized",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/settings/apple/generate-client-secret",
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as auth record",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/settings/apple/generate-client-secret",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
|
||||
},
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin (invalid body)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/settings/apple/generate-client-secret",
|
||||
Body: strings.NewReader(`{`),
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin (empty json)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/settings/apple/generate-client-secret",
|
||||
Body: strings.NewReader(`{}`),
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{
|
||||
`"clientId":{"code":"validation_required"`,
|
||||
`"teamId":{"code":"validation_required"`,
|
||||
`"keyId":{"code":"validation_required"`,
|
||||
`"privateKey":{"code":"validation_required"`,
|
||||
`"duration":{"code":"validation_required"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin (invalid data)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/settings/apple/generate-client-secret",
|
||||
Body: strings.NewReader(`{
|
||||
"clientId": "",
|
||||
"teamId": "123456789",
|
||||
"keyId": "123456789",
|
||||
"privateKey": "invalid",
|
||||
"duration": -1
|
||||
}`),
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{
|
||||
`"clientId":{"code":"validation_required"`,
|
||||
`"teamId":{"code":"validation_length_invalid"`,
|
||||
`"keyId":{"code":"validation_length_invalid"`,
|
||||
`"privateKey":{"code":"validation_match_invalid"`,
|
||||
`"duration":{"code":"validation_min_greater_equal_than_required"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authorized as admin (valid data)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/settings/apple/generate-client-secret",
|
||||
Body: strings.NewReader(fmt.Sprintf(`{
|
||||
"clientId": "123",
|
||||
"teamId": "1234567890",
|
||||
"keyId": "1234567891",
|
||||
"privateKey": %q,
|
||||
"duration": 1
|
||||
}`, privatePem)),
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"secret":"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
+7
-5
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
@@ -11,8 +12,6 @@ import (
|
||||
"github.com/pocketbase/pocketbase/tools/mailer"
|
||||
"github.com/pocketbase/pocketbase/tools/search"
|
||||
"github.com/pocketbase/pocketbase/tools/subscriptions"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
)
|
||||
|
||||
type BaseCollectionEvent struct {
|
||||
@@ -206,9 +205,12 @@ type RecordAuthWithPasswordEvent struct {
|
||||
type RecordAuthWithOAuth2Event struct {
|
||||
BaseCollectionEvent
|
||||
|
||||
HttpContext echo.Context
|
||||
Record *models.Record
|
||||
OAuth2User *auth.AuthUser
|
||||
HttpContext echo.Context
|
||||
ProviderName string
|
||||
ProviderClient auth.Provider
|
||||
Record *models.Record
|
||||
OAuth2User *auth.AuthUser
|
||||
IsNewRecord bool
|
||||
}
|
||||
|
||||
type RecordAuthRefreshEvent struct {
|
||||
|
||||
+87
-8
@@ -1,6 +1,8 @@
|
||||
package daos
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -148,6 +150,11 @@ func (dao *Dao) DeleteCollection(collection *models.Collection) error {
|
||||
}
|
||||
}
|
||||
|
||||
// trigger views resave to check for dependencies
|
||||
if err := txDao.resaveViewsWithChangedSchema(collection.Id); err != nil {
|
||||
return fmt.Errorf("The collection has a view dependency - %w", err)
|
||||
}
|
||||
|
||||
return txDao.Delete(collection)
|
||||
})
|
||||
}
|
||||
@@ -167,7 +174,7 @@ func (dao *Dao) SaveCollection(collection *models.Collection) error {
|
||||
}
|
||||
}
|
||||
|
||||
return dao.RunInTransaction(func(txDao *Dao) error {
|
||||
txErr := dao.RunInTransaction(func(txDao *Dao) error {
|
||||
// set default collection type
|
||||
if collection.Type == "" {
|
||||
collection.Type = models.CollectionTypeBase
|
||||
@@ -175,7 +182,9 @@ func (dao *Dao) SaveCollection(collection *models.Collection) error {
|
||||
|
||||
switch collection.Type {
|
||||
case models.CollectionTypeView:
|
||||
return txDao.saveViewCollection(collection, oldCollection)
|
||||
if err := txDao.saveViewCollection(collection, oldCollection); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// persist the collection model
|
||||
if err := txDao.Save(collection); err != nil {
|
||||
@@ -183,9 +192,23 @@ func (dao *Dao) SaveCollection(collection *models.Collection) error {
|
||||
}
|
||||
|
||||
// sync the changes with the related records table
|
||||
return txDao.SyncRecordTableSchema(collection, oldCollection)
|
||||
if err := txDao.SyncRecordTableSchema(collection, oldCollection); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if txErr != nil {
|
||||
return txErr
|
||||
}
|
||||
|
||||
// trigger an update for all views with changed schema as a result of the current collection save
|
||||
// (ignoring view errors to allow users to update the query from the UI)
|
||||
dao.resaveViewsWithChangedSchema(collection.Id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportCollections imports the provided collections list within a single transaction.
|
||||
@@ -331,8 +354,8 @@ func (dao *Dao) ImportCollections(
|
||||
// - saves the newCollection
|
||||
//
|
||||
// This method returns an error if newCollection is not a "view".
|
||||
func (dao *Dao) saveViewCollection(newCollection *models.Collection, oldCollection *models.Collection) error {
|
||||
if newCollection.IsAuth() {
|
||||
func (dao *Dao) saveViewCollection(newCollection, oldCollection *models.Collection) error {
|
||||
if !newCollection.IsView() {
|
||||
return errors.New("not a view collection")
|
||||
}
|
||||
|
||||
@@ -340,13 +363,13 @@ func (dao *Dao) saveViewCollection(newCollection *models.Collection, oldCollecti
|
||||
query := newCollection.ViewOptions().Query
|
||||
|
||||
// generate collection schema from the query
|
||||
schema, err := txDao.CreateViewSchema(query)
|
||||
viewSchema, err := txDao.CreateViewSchema(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete old renamed view
|
||||
if oldCollection != nil && newCollection.Name != oldCollection.Name {
|
||||
if oldCollection != nil {
|
||||
if err := txDao.DeleteView(oldCollection.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -357,8 +380,64 @@ func (dao *Dao) saveViewCollection(newCollection *models.Collection, oldCollecti
|
||||
return err
|
||||
}
|
||||
|
||||
newCollection.Schema = schema
|
||||
newCollection.Schema = viewSchema
|
||||
|
||||
return txDao.Save(newCollection)
|
||||
})
|
||||
}
|
||||
|
||||
// resaveViewsWithChangedSchema updates all view collections with changed schemas.
|
||||
func (dao *Dao) resaveViewsWithChangedSchema(excludeIds ...string) error {
|
||||
collections, err := dao.FindCollectionsByType(models.CollectionTypeView)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dao.RunInTransaction(func(txDao *Dao) error {
|
||||
for _, collection := range collections {
|
||||
if len(excludeIds) > 0 && list.ExistInSlice(collection.Id, excludeIds) {
|
||||
continue
|
||||
}
|
||||
|
||||
// clone the existing schema so that it is safe for temp modifications
|
||||
oldSchema, err := collection.Schema.Clone()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// generate a new schema from the query
|
||||
newSchema, err := txDao.CreateViewSchema(collection.ViewOptions().Query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// unset the schema field ids to exclude from the comparison
|
||||
for _, f := range oldSchema.Fields() {
|
||||
f.Id = ""
|
||||
}
|
||||
for _, f := range newSchema.Fields() {
|
||||
f.Id = ""
|
||||
}
|
||||
|
||||
encodedNewSchema, err := json.Marshal(newSchema)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encodedOldSchema, err := json.Marshal(oldSchema)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bytes.EqualFold(encodedNewSchema, encodedOldSchema) {
|
||||
continue // no changes
|
||||
}
|
||||
|
||||
if err := txDao.saveViewCollection(collection, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
+72
-5
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
func TestCollectionQuery(t *testing.T) {
|
||||
@@ -186,7 +187,12 @@ func TestDeleteCollection(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
colView, err := app.Dao().FindCollectionByNameOrId("view1")
|
||||
colView1, err := app.Dao().FindCollectionByNameOrId("view1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
colView2, err := app.Dao().FindCollectionByNameOrId("view2")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -199,7 +205,8 @@ func TestDeleteCollection(t *testing.T) {
|
||||
{colAuth, false},
|
||||
{colReferenced, true},
|
||||
{colSystem, true},
|
||||
{colView, false},
|
||||
{colView1, true}, // view2 depend on it
|
||||
{colView2, false},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
@@ -252,7 +259,7 @@ func TestSaveCollectionCreate(t *testing.T) {
|
||||
}
|
||||
|
||||
// check if the records table has the schema fields
|
||||
columns, err := app.Dao().GetTableColumns(collection.Name)
|
||||
columns, err := app.Dao().TableColumns(collection.Name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -291,7 +298,7 @@ func TestSaveCollectionUpdate(t *testing.T) {
|
||||
|
||||
// check if the records table has the schema fields
|
||||
expectedColumns := []string{"id", "created", "updated", "title_update", "test", "files"}
|
||||
columns, err := app.Dao().GetTableColumns(collection.Name)
|
||||
columns, err := app.Dao().TableColumns(collection.Name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -305,6 +312,66 @@ func TestSaveCollectionUpdate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// indirect update of a field used in view should cause view(s) update
|
||||
func TestSaveCollectionIndirectViewsUpdate(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
collection, err := app.Dao().FindCollectionByNameOrId("demo1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// update MaxSelect fields
|
||||
{
|
||||
relMany := collection.Schema.GetFieldByName("rel_many")
|
||||
relManyOpt := relMany.Options.(*schema.RelationOptions)
|
||||
relManyOpt.MaxSelect = types.Pointer(1)
|
||||
|
||||
fileOne := collection.Schema.GetFieldByName("file_one")
|
||||
fileOneOpt := fileOne.Options.(*schema.FileOptions)
|
||||
fileOneOpt.MaxSelect = 10
|
||||
|
||||
if err := app.Dao().SaveCollection(collection); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// check view1 schema
|
||||
{
|
||||
view1, err := app.Dao().FindCollectionByNameOrId("view1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
relMany := view1.Schema.GetFieldByName("rel_many")
|
||||
relManyOpt := relMany.Options.(*schema.RelationOptions)
|
||||
if relManyOpt.MaxSelect == nil || *relManyOpt.MaxSelect != 1 {
|
||||
t.Fatalf("Expected view1.rel_many MaxSelect to be %d, got %v", 1, relManyOpt.MaxSelect)
|
||||
}
|
||||
|
||||
fileOne := view1.Schema.GetFieldByName("file_one")
|
||||
fileOneOpt := fileOne.Options.(*schema.FileOptions)
|
||||
if fileOneOpt.MaxSelect != 10 {
|
||||
t.Fatalf("Expected view1.file_one MaxSelect to be %d, got %v", 10, fileOneOpt.MaxSelect)
|
||||
}
|
||||
}
|
||||
|
||||
// check view2 schema
|
||||
{
|
||||
view2, err := app.Dao().FindCollectionByNameOrId("view2")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
relMany := view2.Schema.GetFieldByName("rel_many")
|
||||
relManyOpt := relMany.Options.(*schema.RelationOptions)
|
||||
if relManyOpt.MaxSelect == nil || *relManyOpt.MaxSelect != 1 {
|
||||
t.Fatalf("Expected view2.rel_many MaxSelect to be %d, got %v", 1, relManyOpt.MaxSelect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCollections(t *testing.T) {
|
||||
totalCollections := 10
|
||||
|
||||
@@ -435,7 +502,7 @@ func TestImportCollections(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"id":"wsmn24bux7wo113",
|
||||
"name":"demo",
|
||||
"name":"demo1_rename",
|
||||
"schema":[
|
||||
{
|
||||
"id":"_2hlxbmp",
|
||||
|
||||
+9
-7
@@ -247,9 +247,9 @@ func (dao *Dao) IsRecordValueUnique(
|
||||
var normalizedVal any
|
||||
switch val := value.(type) {
|
||||
case []string:
|
||||
normalizedVal = append(types.JsonArray{}, list.ToInterfaceSlice(val)...)
|
||||
normalizedVal = append(types.JsonArray[string]{}, val...)
|
||||
case []any:
|
||||
normalizedVal = append(types.JsonArray{}, val...)
|
||||
normalizedVal = append(types.JsonArray[any]{}, val...)
|
||||
default:
|
||||
normalizedVal = val
|
||||
}
|
||||
@@ -472,14 +472,16 @@ func (dao *Dao) cascadeRecordDelete(mainRecord *models.Record, refs map[*models.
|
||||
recordTableName := inflector.Columnify(refCollection.Name)
|
||||
prefixedFieldName := recordTableName + "." + inflector.Columnify(field.Name)
|
||||
|
||||
// @todo optimize single relation lookup
|
||||
query := dao.RecordQuery(refCollection).
|
||||
Distinct(true).
|
||||
InnerJoin(fmt.Sprintf(
|
||||
// note: the case is used to normalize the value access
|
||||
query := dao.RecordQuery(refCollection).Distinct(true)
|
||||
|
||||
if opt, ok := field.Options.(schema.MultiValuer); !ok || !opt.IsMultiple() {
|
||||
query.AndWhere(dbx.HashExp{prefixedFieldName: mainRecord.Id})
|
||||
} else {
|
||||
query.InnerJoin(fmt.Sprintf(
|
||||
`json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END) as {{%s}}`,
|
||||
prefixedFieldName, prefixedFieldName, prefixedFieldName, uniqueJsonEachAlias,
|
||||
), dbx.HashExp{uniqueJsonEachAlias + ".value": mainRecord.Id})
|
||||
}
|
||||
|
||||
if refCollection.Id == mainRecord.Collection().Id {
|
||||
query.AndWhere(dbx.Not(dbx.HashExp{recordTableName + ".id": mainRecord.Id}))
|
||||
|
||||
+14
-2
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
"github.com/pocketbase/pocketbase/tools/dbutils"
|
||||
"github.com/pocketbase/pocketbase/tools/inflector"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
@@ -86,7 +87,7 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch
|
||||
if indirectRelFieldOptions == nil || indirectRelFieldOptions.CollectionId != mainCollection.Id {
|
||||
return fmt.Errorf("Invalid indirect relation field path %q.", parts[0])
|
||||
}
|
||||
if indirectRelFieldOptions.MaxSelect != nil && *indirectRelFieldOptions.MaxSelect != 1 {
|
||||
if indirectRelFieldOptions.IsMultiple() {
|
||||
// for now don't allow multi-relation indirect fields expand
|
||||
// due to eventual poor query performance with large data sets.
|
||||
return fmt.Errorf("Multi-relation fields cannot be indirectly expanded in %q.", parts[0])
|
||||
@@ -126,7 +127,7 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch
|
||||
MaxSelect: nil,
|
||||
CollectionId: indirectRel.Id,
|
||||
}
|
||||
if indirectRelField.Unique {
|
||||
if isRelFieldUnique(indirectRel, indirectRelField.Name) {
|
||||
relFieldOptions.MaxSelect = types.Pointer(1)
|
||||
}
|
||||
// indirect relation
|
||||
@@ -269,3 +270,14 @@ func normalizeExpands(paths []string) []string {
|
||||
|
||||
return list.ToUniqueStringSlice(result)
|
||||
}
|
||||
|
||||
func isRelFieldUnique(collection *models.Collection, fieldName string) bool {
|
||||
for _, idx := range collection.Indexes {
|
||||
parsed := dbutils.ParseIndex(idx)
|
||||
if parsed.Unique && len(parsed.Columns) == 1 && strings.EqualFold(parsed.Columns[0].Name, fieldName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -362,3 +362,57 @@ func TestExpandRecord(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndirectExpandSingeVsArrayResult(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
record, err := app.Dao().FindRecordById("demo3", "7nwo8tuiatetxdm")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// non-unique indirect expand
|
||||
{
|
||||
errs := app.Dao().ExpandRecord(record, []string{"demo4(rel_one_cascade)"}, func(c *models.Collection, ids []string) ([]*models.Record, error) {
|
||||
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
|
||||
})
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
|
||||
result, ok := record.Expand()["demo4(rel_one_cascade)"].([]*models.Record)
|
||||
if !ok {
|
||||
t.Fatalf("Expected the expanded result to be a slice, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
// mock a unique constraint for the rel_one_cascade field
|
||||
{
|
||||
demo4, err := app.Dao().FindCollectionByNameOrId("demo4")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
demo4.Indexes = append(demo4.Indexes, "create unique index idx_unique_expand on demo4 (rel_one_cascade)")
|
||||
|
||||
if err := app.Dao().SaveCollection(demo4); err != nil {
|
||||
t.Fatalf("Failed to mock unique constraint: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// non-unique indirect expand
|
||||
{
|
||||
errs := app.Dao().ExpandRecord(record, []string{"demo4(rel_one_cascade)"}, func(c *models.Collection, ids []string) ([]*models.Record, error) {
|
||||
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
|
||||
})
|
||||
if len(errs) > 0 {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
|
||||
result, ok := record.Expand()["demo4(rel_one_cascade)"].(*models.Record)
|
||||
if !ok {
|
||||
t.Fatalf("Expected the expanded result to be a single model, got %v", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
+235
-61
@@ -2,10 +2,14 @@ package daos
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
"github.com/pocketbase/pocketbase/tools/dbutils"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
@@ -15,70 +19,67 @@ import (
|
||||
//
|
||||
// If `oldCollection` is null, then only `newCollection` is used to create the record table.
|
||||
func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldCollection *models.Collection) error {
|
||||
// create
|
||||
if oldCollection == nil {
|
||||
cols := map[string]string{
|
||||
schema.FieldNameId: "TEXT PRIMARY KEY NOT NULL",
|
||||
schema.FieldNameCreated: "TEXT DEFAULT '' NOT NULL",
|
||||
schema.FieldNameUpdated: "TEXT DEFAULT '' NOT NULL",
|
||||
}
|
||||
return dao.RunInTransaction(func(txDao *Dao) error {
|
||||
// create
|
||||
// -----------------------------------------------------------
|
||||
if oldCollection == nil {
|
||||
cols := map[string]string{
|
||||
schema.FieldNameId: "TEXT PRIMARY KEY DEFAULT ('r'||lower(hex(randomblob(7)))) NOT NULL",
|
||||
schema.FieldNameCreated: "TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL",
|
||||
schema.FieldNameUpdated: "TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL",
|
||||
}
|
||||
|
||||
if newCollection.IsAuth() {
|
||||
cols[schema.FieldNameUsername] = "TEXT NOT NULL"
|
||||
cols[schema.FieldNameEmail] = "TEXT DEFAULT '' NOT NULL"
|
||||
cols[schema.FieldNameEmailVisibility] = "BOOLEAN DEFAULT FALSE NOT NULL"
|
||||
cols[schema.FieldNameVerified] = "BOOLEAN DEFAULT FALSE NOT NULL"
|
||||
cols[schema.FieldNameTokenKey] = "TEXT NOT NULL"
|
||||
cols[schema.FieldNamePasswordHash] = "TEXT NOT NULL"
|
||||
cols[schema.FieldNameLastResetSentAt] = "TEXT DEFAULT '' NOT NULL"
|
||||
cols[schema.FieldNameLastVerificationSentAt] = "TEXT DEFAULT '' NOT NULL"
|
||||
}
|
||||
if newCollection.IsAuth() {
|
||||
cols[schema.FieldNameUsername] = "TEXT NOT NULL"
|
||||
cols[schema.FieldNameEmail] = "TEXT DEFAULT '' NOT NULL"
|
||||
cols[schema.FieldNameEmailVisibility] = "BOOLEAN DEFAULT FALSE NOT NULL"
|
||||
cols[schema.FieldNameVerified] = "BOOLEAN DEFAULT FALSE NOT NULL"
|
||||
cols[schema.FieldNameTokenKey] = "TEXT NOT NULL"
|
||||
cols[schema.FieldNamePasswordHash] = "TEXT NOT NULL"
|
||||
cols[schema.FieldNameLastResetSentAt] = "TEXT DEFAULT '' NOT NULL"
|
||||
cols[schema.FieldNameLastVerificationSentAt] = "TEXT DEFAULT '' NOT NULL"
|
||||
}
|
||||
|
||||
// ensure that the new collection has an id
|
||||
if !newCollection.HasId() {
|
||||
newCollection.RefreshId()
|
||||
newCollection.MarkAsNew()
|
||||
}
|
||||
// ensure that the new collection has an id
|
||||
if !newCollection.HasId() {
|
||||
newCollection.RefreshId()
|
||||
newCollection.MarkAsNew()
|
||||
}
|
||||
|
||||
tableName := newCollection.Name
|
||||
tableName := newCollection.Name
|
||||
|
||||
// add schema field definitions
|
||||
for _, field := range newCollection.Schema.Fields() {
|
||||
cols[field.Name] = field.ColDefinition()
|
||||
}
|
||||
// add schema field definitions
|
||||
for _, field := range newCollection.Schema.Fields() {
|
||||
cols[field.Name] = field.ColDefinition()
|
||||
}
|
||||
|
||||
// create table
|
||||
if _, err := dao.DB().CreateTable(tableName, cols).Execute(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add named index on the base `created` column
|
||||
if _, err := dao.DB().CreateIndex(tableName, "_"+newCollection.Id+"_created_idx", "created").Execute(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add named unique index on the email and tokenKey columns
|
||||
if newCollection.IsAuth() {
|
||||
_, err := dao.DB().NewQuery(fmt.Sprintf(
|
||||
`
|
||||
CREATE UNIQUE INDEX _%s_username_idx ON {{%s}} ([[username]]);
|
||||
CREATE UNIQUE INDEX _%s_email_idx ON {{%s}} ([[email]]) WHERE [[email]] != '';
|
||||
CREATE UNIQUE INDEX _%s_tokenKey_idx ON {{%s}} ([[tokenKey]]);
|
||||
`,
|
||||
newCollection.Id, tableName,
|
||||
newCollection.Id, tableName,
|
||||
newCollection.Id, tableName,
|
||||
)).Execute()
|
||||
if err != nil {
|
||||
// create table
|
||||
if _, err := txDao.DB().CreateTable(tableName, cols).Execute(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add named unique index on the email and tokenKey columns
|
||||
if newCollection.IsAuth() {
|
||||
_, err := txDao.DB().NewQuery(fmt.Sprintf(
|
||||
`
|
||||
CREATE UNIQUE INDEX _%s_username_idx ON {{%s}} ([[username]]);
|
||||
CREATE UNIQUE INDEX _%s_email_idx ON {{%s}} ([[email]]) WHERE [[email]] != '';
|
||||
CREATE UNIQUE INDEX _%s_tokenKey_idx ON {{%s}} ([[tokenKey]]);
|
||||
`,
|
||||
newCollection.Id, tableName,
|
||||
newCollection.Id, tableName,
|
||||
newCollection.Id, tableName,
|
||||
)).Execute()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return txDao.createCollectionIndexes(newCollection)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// update
|
||||
return dao.RunInTransaction(func(txDao *Dao) error {
|
||||
// update
|
||||
// -----------------------------------------------------------
|
||||
oldTableName := oldCollection.Name
|
||||
newTableName := newCollection.Name
|
||||
oldSchema := oldCollection.Schema
|
||||
@@ -86,9 +87,14 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle
|
||||
deletedFieldNames := []string{}
|
||||
renamedFieldNames := map[string]string{}
|
||||
|
||||
// drop old indexes (if any)
|
||||
if err := txDao.dropCollectionIndex(oldCollection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check for renamed table
|
||||
if !strings.EqualFold(oldTableName, newTableName) {
|
||||
_, err := txDao.DB().RenameTable(oldTableName, newTableName).Execute()
|
||||
_, err := txDao.DB().RenameTable("{{"+oldTableName+"}}", "{{"+newTableName+"}}").Execute()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -102,7 +108,7 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle
|
||||
|
||||
_, err := txDao.DB().DropColumn(newTableName, oldField.Name).Execute()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to drop column %s - %w", oldField.Name, err)
|
||||
}
|
||||
|
||||
deletedFieldNames = append(deletedFieldNames, oldField.Name)
|
||||
@@ -125,7 +131,7 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle
|
||||
// add
|
||||
_, err := txDao.DB().AddColumn(newTableName, tempName, field.ColDefinition()).Execute()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to add column %s - %w", field.Name, err)
|
||||
}
|
||||
} else if oldField.Name != field.Name {
|
||||
tempName := field.Name + security.PseudorandomString(5)
|
||||
@@ -134,7 +140,7 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle
|
||||
// rename
|
||||
_, err := txDao.DB().RenameColumn(newTableName, oldField.Name, tempName).Execute()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to rename column %s - %w", oldField.Name, err)
|
||||
}
|
||||
|
||||
renamedFieldNames[oldField.Name] = field.Name
|
||||
@@ -149,11 +155,109 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle
|
||||
}
|
||||
}
|
||||
|
||||
return txDao.syncCollectionReferences(newCollection, renamedFieldNames, deletedFieldNames)
|
||||
if err := txDao.normalizeSingleVsMultipleFieldChanges(newCollection, oldCollection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := txDao.syncRelationDisplayFieldsChanges(newCollection, renamedFieldNames, deletedFieldNames); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return txDao.createCollectionIndexes(newCollection)
|
||||
})
|
||||
}
|
||||
|
||||
func (dao *Dao) syncCollectionReferences(collection *models.Collection, renamedFieldNames map[string]string, deletedFieldNames []string) error {
|
||||
func (dao *Dao) normalizeSingleVsMultipleFieldChanges(newCollection, oldCollection *models.Collection) error {
|
||||
if newCollection.IsView() || oldCollection == nil {
|
||||
return nil // view or not an update
|
||||
}
|
||||
|
||||
return dao.RunInTransaction(func(txDao *Dao) error {
|
||||
for _, newField := range newCollection.Schema.Fields() {
|
||||
oldField := oldCollection.Schema.GetFieldById(newField.Id)
|
||||
if oldField == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var isNewMultiple bool
|
||||
if opt, ok := newField.Options.(schema.MultiValuer); ok {
|
||||
isNewMultiple = opt.IsMultiple()
|
||||
}
|
||||
|
||||
var isOldMultiple bool
|
||||
if opt, ok := oldField.Options.(schema.MultiValuer); ok {
|
||||
isOldMultiple = opt.IsMultiple()
|
||||
}
|
||||
|
||||
if isOldMultiple == isNewMultiple {
|
||||
continue // no change
|
||||
}
|
||||
|
||||
var updateQuery *dbx.Query
|
||||
|
||||
if !isOldMultiple && isNewMultiple {
|
||||
// single -> multiple (convert to array)
|
||||
updateQuery = txDao.DB().NewQuery(fmt.Sprintf(
|
||||
`UPDATE {{%s}} set [[%s]] = (
|
||||
CASE
|
||||
WHEN COALESCE([[%s]], '') = ''
|
||||
THEN '[]'
|
||||
ELSE (
|
||||
CASE
|
||||
WHEN json_valid([[%s]]) AND json_type([[%s]]) == 'array'
|
||||
THEN [[%s]]
|
||||
ELSE json_array([[%s]])
|
||||
END
|
||||
)
|
||||
END
|
||||
)`,
|
||||
newCollection.Name,
|
||||
newField.Name,
|
||||
newField.Name,
|
||||
newField.Name,
|
||||
newField.Name,
|
||||
newField.Name,
|
||||
newField.Name,
|
||||
))
|
||||
} else {
|
||||
// multiple -> single (keep only the last element)
|
||||
//
|
||||
// note: for file fields the actual files are not deleted
|
||||
// allowing additional custom handling via migration.
|
||||
updateQuery = txDao.DB().NewQuery(fmt.Sprintf(
|
||||
`UPDATE {{%s}} set [[%s]] = (
|
||||
CASE
|
||||
WHEN COALESCE([[%s]], '[]') = '[]'
|
||||
THEN ''
|
||||
ELSE (
|
||||
CASE
|
||||
WHEN json_valid([[%s]]) AND json_type([[%s]]) == 'array'
|
||||
THEN COALESCE(json_extract([[%s]], '$[#-1]'), '')
|
||||
ELSE [[%s]]
|
||||
END
|
||||
)
|
||||
END
|
||||
)`,
|
||||
newCollection.Name,
|
||||
newField.Name,
|
||||
newField.Name,
|
||||
newField.Name,
|
||||
newField.Name,
|
||||
newField.Name,
|
||||
newField.Name,
|
||||
))
|
||||
}
|
||||
|
||||
if _, err := updateQuery.Execute(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (dao *Dao) syncRelationDisplayFieldsChanges(collection *models.Collection, renamedFieldNames map[string]string, deletedFieldNames []string) error {
|
||||
if len(renamedFieldNames) == 0 && len(deletedFieldNames) == 0 {
|
||||
return nil // nothing to sync
|
||||
}
|
||||
@@ -196,3 +300,73 @@ func (dao *Dao) syncCollectionReferences(collection *models.Collection, renamedF
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dao *Dao) dropCollectionIndex(collection *models.Collection) error {
|
||||
if collection.IsView() {
|
||||
return nil // views don't have indexes
|
||||
}
|
||||
|
||||
return dao.RunInTransaction(func(txDao *Dao) error {
|
||||
for _, raw := range collection.Indexes {
|
||||
parsed := dbutils.ParseIndex(raw)
|
||||
|
||||
if !parsed.IsValid() {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := txDao.DB().NewQuery(fmt.Sprintf("DROP INDEX IF EXISTS [[%s]]", parsed.IndexName)).Execute(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (dao *Dao) createCollectionIndexes(collection *models.Collection) error {
|
||||
if collection.IsView() {
|
||||
return nil // views don't have indexes
|
||||
}
|
||||
|
||||
return dao.RunInTransaction(func(txDao *Dao) error {
|
||||
// drop new indexes in case a duplicated index name is used
|
||||
if err := txDao.dropCollectionIndex(collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// upsert new indexes
|
||||
//
|
||||
// note: we are returning validation errors because the indexes cannot be
|
||||
// validated in a form, aka. before persisting the related collection
|
||||
// record table changes
|
||||
errs := validation.Errors{}
|
||||
for i, idx := range collection.Indexes {
|
||||
parsed := dbutils.ParseIndex(idx)
|
||||
|
||||
// ensure that the index is always for the current collection
|
||||
parsed.TableName = collection.Name
|
||||
|
||||
if !parsed.IsValid() {
|
||||
errs[strconv.Itoa(i)] = validation.NewError(
|
||||
"validation_invalid_index_expression",
|
||||
"Invalid CREATE INDEX expression.",
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := txDao.DB().NewQuery(parsed.Build()).Execute(); err != nil {
|
||||
errs[strconv.Itoa(i)] = validation.NewError(
|
||||
"validation_invalid_index_expression",
|
||||
fmt.Sprintf("Failed to create index %s - %v.", parsed.IndexName, err.Error()),
|
||||
)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return validation.Errors{"indexes": errs}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
+162
-22
@@ -1,12 +1,16 @@
|
||||
package daos_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
func TestSyncRecordTableSchema(t *testing.T) {
|
||||
@@ -36,15 +40,17 @@ func TestSyncRecordTableSchema(t *testing.T) {
|
||||
Type: schema.FieldTypeEmail,
|
||||
},
|
||||
)
|
||||
updatedCollection.Indexes = types.JsonArray[string]{"create index idx_title_renamed on anything (title_renamed)"}
|
||||
|
||||
scenarios := []struct {
|
||||
newCollection *models.Collection
|
||||
oldCollection *models.Collection
|
||||
expectedTableName string
|
||||
expectedColumns []string
|
||||
name string
|
||||
newCollection *models.Collection
|
||||
oldCollection *models.Collection
|
||||
expectedColumns []string
|
||||
expectedIndexesCount int
|
||||
}{
|
||||
// new base collection
|
||||
{
|
||||
"new base collection",
|
||||
&models.Collection{
|
||||
Name: "new_table",
|
||||
Schema: schema.NewSchema(
|
||||
@@ -55,11 +61,11 @@ func TestSyncRecordTableSchema(t *testing.T) {
|
||||
),
|
||||
},
|
||||
nil,
|
||||
"new_table",
|
||||
[]string{"id", "created", "updated", "test"},
|
||||
0,
|
||||
},
|
||||
// new auth collection
|
||||
{
|
||||
"new auth collection",
|
||||
&models.Collection{
|
||||
Name: "new_table_auth",
|
||||
Type: models.CollectionTypeAuth,
|
||||
@@ -69,51 +75,185 @@ func TestSyncRecordTableSchema(t *testing.T) {
|
||||
Type: schema.FieldTypeText,
|
||||
},
|
||||
),
|
||||
Indexes: types.JsonArray[string]{"create index idx_auth_test on anything (email, username)"},
|
||||
},
|
||||
nil,
|
||||
"new_table_auth",
|
||||
[]string{
|
||||
"id", "created", "updated", "test",
|
||||
"username", "email", "verified", "emailVisibility",
|
||||
"tokenKey", "passwordHash", "lastResetSentAt", "lastVerificationSentAt",
|
||||
},
|
||||
4,
|
||||
},
|
||||
// no changes
|
||||
{
|
||||
"no changes",
|
||||
oldCollection,
|
||||
oldCollection,
|
||||
"demo3",
|
||||
[]string{"id", "created", "updated", "title", "active"},
|
||||
3,
|
||||
},
|
||||
// renamed table, deleted column, renamed columnd and new column
|
||||
{
|
||||
"renamed table, deleted column, renamed columnd and new column",
|
||||
updatedCollection,
|
||||
oldCollection,
|
||||
"demo_renamed",
|
||||
[]string{"id", "created", "updated", "title_renamed", "new_field"},
|
||||
1,
|
||||
},
|
||||
}
|
||||
|
||||
for i, scenario := range scenarios {
|
||||
err := app.Dao().SyncRecordTableSchema(scenario.newCollection, scenario.oldCollection)
|
||||
for _, s := range scenarios {
|
||||
err := app.Dao().SyncRecordTableSchema(s.newCollection, s.oldCollection)
|
||||
if err != nil {
|
||||
t.Errorf("(%d) %v", i, err)
|
||||
t.Errorf("[%s] %v", s.name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !app.Dao().HasTable(scenario.newCollection.Name) {
|
||||
t.Errorf("(%d) Expected table %s to exist", i, scenario.newCollection.Name)
|
||||
if !app.Dao().HasTable(s.newCollection.Name) {
|
||||
t.Errorf("[%s] Expected table %s to exist", s.name, s.newCollection.Name)
|
||||
}
|
||||
|
||||
cols, _ := app.Dao().GetTableColumns(scenario.newCollection.Name)
|
||||
if len(cols) != len(scenario.expectedColumns) {
|
||||
t.Errorf("(%d) Expected columns %v, got %v", i, scenario.expectedColumns, cols)
|
||||
cols, _ := app.Dao().TableColumns(s.newCollection.Name)
|
||||
if len(cols) != len(s.expectedColumns) {
|
||||
t.Errorf("[%s] Expected columns %v, got %v", s.name, s.expectedColumns, cols)
|
||||
}
|
||||
|
||||
for _, c := range cols {
|
||||
if !list.ExistInSlice(c, scenario.expectedColumns) {
|
||||
t.Errorf("(%d) Couldn't find column %s in %v", i, c, scenario.expectedColumns)
|
||||
if !list.ExistInSlice(c, s.expectedColumns) {
|
||||
t.Errorf("[%s] Couldn't find column %s in %v", s.name, c, s.expectedColumns)
|
||||
}
|
||||
}
|
||||
|
||||
indexes, _ := app.Dao().TableIndexes(s.newCollection.Name)
|
||||
|
||||
if totalIndexes := len(indexes); totalIndexes != s.expectedIndexesCount {
|
||||
t.Errorf("[%s] Expected %d indexes, got %d:\n%v", s.name, s.expectedIndexesCount, totalIndexes, indexes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleVsMultipleValuesNormalization(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
collection, err := app.Dao().FindCollectionByNameOrId("demo1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// mock field changes
|
||||
{
|
||||
selectOneField := collection.Schema.GetFieldByName("select_one")
|
||||
opt := selectOneField.Options.(*schema.SelectOptions)
|
||||
opt.MaxSelect = 2
|
||||
}
|
||||
{
|
||||
selectManyField := collection.Schema.GetFieldByName("select_many")
|
||||
opt := selectManyField.Options.(*schema.SelectOptions)
|
||||
opt.MaxSelect = 1
|
||||
}
|
||||
{
|
||||
fileOneField := collection.Schema.GetFieldByName("file_one")
|
||||
opt := fileOneField.Options.(*schema.FileOptions)
|
||||
opt.MaxSelect = 2
|
||||
}
|
||||
{
|
||||
fileManyField := collection.Schema.GetFieldByName("file_many")
|
||||
opt := fileManyField.Options.(*schema.FileOptions)
|
||||
opt.MaxSelect = 1
|
||||
}
|
||||
{
|
||||
relOneField := collection.Schema.GetFieldByName("rel_one")
|
||||
opt := relOneField.Options.(*schema.RelationOptions)
|
||||
opt.MaxSelect = types.Pointer(2)
|
||||
}
|
||||
{
|
||||
relManyField := collection.Schema.GetFieldByName("rel_many")
|
||||
opt := relManyField.Options.(*schema.RelationOptions)
|
||||
opt.MaxSelect = types.Pointer(1)
|
||||
}
|
||||
|
||||
if err := app.Dao().SaveCollection(collection); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type expectation struct {
|
||||
SelectOne string `db:"select_one"`
|
||||
SelectMany string `db:"select_many"`
|
||||
FileOne string `db:"file_one"`
|
||||
FileMany string `db:"file_many"`
|
||||
RelOne string `db:"rel_one"`
|
||||
RelMany string `db:"rel_many"`
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
recordId string
|
||||
expected expectation
|
||||
}{
|
||||
{
|
||||
"imy661ixudk5izi",
|
||||
expectation{
|
||||
SelectOne: `[]`,
|
||||
SelectMany: ``,
|
||||
FileOne: `[]`,
|
||||
FileMany: ``,
|
||||
RelOne: `[]`,
|
||||
RelMany: ``,
|
||||
},
|
||||
},
|
||||
{
|
||||
"al1h9ijdeojtsjy",
|
||||
expectation{
|
||||
SelectOne: `["optionB"]`,
|
||||
SelectMany: `optionB`,
|
||||
FileOne: `["300_Jsjq7RdBgA.png"]`,
|
||||
FileMany: ``,
|
||||
RelOne: `["84nmscqy84lsi1t"]`,
|
||||
RelMany: `oap640cot4yru2s`,
|
||||
},
|
||||
},
|
||||
{
|
||||
"84nmscqy84lsi1t",
|
||||
expectation{
|
||||
SelectOne: `["optionB"]`,
|
||||
SelectMany: `optionC`,
|
||||
FileOne: `["test_d61b33QdDU.txt"]`,
|
||||
FileMany: `test_tC1Yc87DfC.txt`,
|
||||
RelOne: `[]`,
|
||||
RelMany: `oap640cot4yru2s`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
result := new(expectation)
|
||||
|
||||
err := app.Dao().DB().Select(
|
||||
"select_one",
|
||||
"select_many",
|
||||
"file_one",
|
||||
"file_many",
|
||||
"rel_one",
|
||||
"rel_many",
|
||||
).From(collection.Name).Where(dbx.HashExp{"id": s.recordId}).One(result)
|
||||
if err != nil {
|
||||
t.Errorf("[%s] Failed to load record: %v", s.recordId, err)
|
||||
continue
|
||||
}
|
||||
|
||||
encodedResult, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Errorf("[%s] Failed to encode result: %v", s.recordId, err)
|
||||
continue
|
||||
}
|
||||
|
||||
encodedExpectation, err := json.Marshal(s.expected)
|
||||
if err != nil {
|
||||
t.Errorf("[%s] Failed to encode expectation: %v", s.recordId, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !bytes.EqualFold(encodedExpectation, encodedResult) {
|
||||
t.Errorf("[%s] Expected \n%s, \ngot \n%s", s.recordId, encodedExpectation, encodedResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -748,13 +748,13 @@ func TestDeleteRecord(t *testing.T) {
|
||||
}
|
||||
// ensure that the json rel fields were prefixed
|
||||
joinedQueries := strings.Join(calledQueries, " ")
|
||||
expectedRelManyJoin := "`demo1` INNER JOIN json_each(CASE WHEN json_valid([[demo1.rel_many]]) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END)"
|
||||
if !strings.Contains(joinedQueries, expectedRelManyJoin) {
|
||||
t.Fatalf("(rec3) Expected the cascade delete to call the query \n%v, got \n%v", expectedRelManyJoin, calledQueries)
|
||||
expectedRelManyPart := "`demo1` INNER JOIN json_each(CASE WHEN json_valid([[demo1.rel_many]]) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END)"
|
||||
if !strings.Contains(joinedQueries, expectedRelManyPart) {
|
||||
t.Fatalf("(rec3) Expected the cascade delete to call the query \n%v, got \n%v", expectedRelManyPart, calledQueries)
|
||||
}
|
||||
expectedRelOneJoin := "`demo1` INNER JOIN json_each(CASE WHEN json_valid([[demo1.rel_one]]) THEN [[demo1.rel_one]] ELSE json_array([[demo1.rel_one]]) END)"
|
||||
if !strings.Contains(joinedQueries, expectedRelOneJoin) {
|
||||
t.Fatalf("(rec3) Expected the cascade delete to call the query \n%v, got \n%v", expectedRelOneJoin, calledQueries)
|
||||
expectedRelOnePart := "SELECT DISTINCT `demo1`.* FROM `demo1` WHERE (`demo1`.`rel_one`="
|
||||
if !strings.Contains(joinedQueries, expectedRelOnePart) {
|
||||
t.Fatalf("(rec3) Expected the cascade delete to call the query \n%v, got \n%v", expectedRelOnePart, calledQueries)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ func (dao *Dao) DeleteOldRequests(createdBefore time.Time) error {
|
||||
formattedDate := createdBefore.UTC().Format(types.DefaultDateLayout)
|
||||
expr := dbx.NewExp("[[created]] <= {:date}", dbx.Params{"date": formattedDate})
|
||||
|
||||
_, err := dao.DB().Delete(tableName, expr).Execute()
|
||||
_, err := dao.NonconcurrentDB().Delete(tableName, expr).Execute()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
+34
-4
@@ -21,8 +21,8 @@ func (dao *Dao) HasTable(tableName string) bool {
|
||||
return err == nil && exists
|
||||
}
|
||||
|
||||
// GetTableColumns returns all column names of a single table by its name.
|
||||
func (dao *Dao) GetTableColumns(tableName string) ([]string, error) {
|
||||
// TableColumns returns all column names of a single table by its name.
|
||||
func (dao *Dao) TableColumns(tableName string) ([]string, error) {
|
||||
columns := []string{}
|
||||
|
||||
err := dao.DB().NewQuery("SELECT name FROM PRAGMA_TABLE_INFO({:tableName})").
|
||||
@@ -32,8 +32,8 @@ func (dao *Dao) GetTableColumns(tableName string) ([]string, error) {
|
||||
return columns, err
|
||||
}
|
||||
|
||||
// GetTableInfo returns the `table_info` pragma result for the specified table.
|
||||
func (dao *Dao) GetTableInfo(tableName string) ([]*models.TableInfoRow, error) {
|
||||
// TableInfo returns the `table_info` pragma result for the specified table.
|
||||
func (dao *Dao) TableInfo(tableName string) ([]*models.TableInfoRow, error) {
|
||||
info := []*models.TableInfoRow{}
|
||||
|
||||
err := dao.DB().NewQuery("SELECT * FROM PRAGMA_TABLE_INFO({:tableName})").
|
||||
@@ -52,6 +52,36 @@ func (dao *Dao) GetTableInfo(tableName string) ([]*models.TableInfoRow, error) {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// TableIndexes returns a name grouped map with all non empty index of the specified table.
|
||||
//
|
||||
// Note: This method doesn't return an error on nonexisting table.
|
||||
func (dao *Dao) TableIndexes(tableName string) (map[string]string, error) {
|
||||
indexes := []struct {
|
||||
Name string
|
||||
Sql string
|
||||
}{}
|
||||
|
||||
err := dao.DB().Select("name", "sql").
|
||||
From("sqlite_master").
|
||||
AndWhere(dbx.NewExp("sql is not null")).
|
||||
AndWhere(dbx.HashExp{
|
||||
"type": "index",
|
||||
"tbl_name": tableName,
|
||||
}).
|
||||
All(&indexes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]string, len(indexes))
|
||||
|
||||
for _, idx := range indexes {
|
||||
result[idx.Name] = idx.Sql
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DeleteTable drops the specified table.
|
||||
//
|
||||
// This method is a no-op if a table with the provided name doesn't exist.
|
||||
|
||||
+46
-4
@@ -35,7 +35,7 @@ func TestHasTable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTableColumns(t *testing.T) {
|
||||
func TestTableColumns(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
@@ -48,7 +48,7 @@ func TestGetTableColumns(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
columns, _ := app.Dao().GetTableColumns(s.tableName)
|
||||
columns, _ := app.Dao().TableColumns(s.tableName)
|
||||
|
||||
if len(columns) != len(s.expected) {
|
||||
t.Errorf("[%d] Expected columns %v, got %v", i, s.expected, columns)
|
||||
@@ -63,7 +63,7 @@ func TestGetTableColumns(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTableInfo(t *testing.T) {
|
||||
func TestTableInfo(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
@@ -80,7 +80,7 @@ func TestGetTableInfo(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
rows, _ := app.Dao().GetTableInfo(s.tableName)
|
||||
rows, _ := app.Dao().TableInfo(s.tableName)
|
||||
|
||||
raw, _ := json.Marshal(rows)
|
||||
rawStr := string(raw)
|
||||
@@ -139,3 +139,45 @@ func TestVacuum(t *testing.T) {
|
||||
t.Fatalf("Expected VACUUM query, got %s", calledQueries[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableIndexes(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
scenarios := []struct {
|
||||
table string
|
||||
expectError bool
|
||||
expectIndexes []string
|
||||
}{
|
||||
{
|
||||
"missing",
|
||||
false,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"demo2",
|
||||
false,
|
||||
[]string{"idx_demo2_created", "idx_unique_demo2_title", "idx_demo2_active"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
result, err := app.Dao().TableIndexes(s.table)
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Errorf("[%s] Expected hasErr %v, got %v", s.table, s.expectError, hasErr)
|
||||
}
|
||||
|
||||
if len(s.expectIndexes) != len(result) {
|
||||
t.Errorf("[%s] Expected %d indexes, got %d:\n%v", s.table, len(s.expectIndexes), len(result), result)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, name := range s.expectIndexes {
|
||||
if result[name] == "" {
|
||||
t.Errorf("[%s] Missing index %q in \n%v", s.table, name, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-14
@@ -63,7 +63,11 @@ func (dao *Dao) SaveView(name string, selectQuery string) error {
|
||||
|
||||
// fetch the view table info to ensure that the view was created
|
||||
// because missing tables or columns won't return an error
|
||||
if _, err := txDao.GetTableInfo(name); err != nil {
|
||||
if _, err := txDao.TableInfo(name); err != nil {
|
||||
// manually cleanup previously created view in case the func
|
||||
// is called in a nested transaction and the error is discarded
|
||||
txDao.DeleteView(name)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -95,7 +99,7 @@ func (dao *Dao) CreateViewSchema(selectQuery string) (schema.Schema, error) {
|
||||
defer txDao.DeleteView(tempView)
|
||||
|
||||
// extract the generated view table info
|
||||
info, err := txDao.GetTableInfo(tempView)
|
||||
info, err := txDao.TableInfo(tempView)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -189,15 +193,18 @@ func (dao *Dao) FindRecordByViewFile(
|
||||
|
||||
record := &models.Record{}
|
||||
|
||||
err = dao.RecordQuery(qf.collection).
|
||||
InnerJoin(fmt.Sprintf(
|
||||
// note: the case is used to normalize the value access
|
||||
query := dao.RecordQuery(qf.collection).Limit(1)
|
||||
|
||||
if opt, ok := qf.original.Options.(schema.MultiValuer); !ok || !opt.IsMultiple() {
|
||||
query.AndWhere(dbx.HashExp{cleanFieldName: filename})
|
||||
} else {
|
||||
query.InnerJoin(fmt.Sprintf(
|
||||
`json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END) as {{_je_file}}`,
|
||||
cleanFieldName, cleanFieldName, cleanFieldName,
|
||||
), dbx.HashExp{"_je_file.value": filename}).
|
||||
Limit(1).
|
||||
One(record)
|
||||
if err != nil {
|
||||
), dbx.HashExp{"_je_file.value": filename})
|
||||
}
|
||||
|
||||
if err := query.One(record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -272,16 +279,13 @@ func (dao *Dao) parseQueryToFields(selectQuery string) (map[string]*queryField,
|
||||
|
||||
var fieldName string
|
||||
var collection *models.Collection
|
||||
var isMainTableField bool
|
||||
|
||||
if len(parts) == 2 {
|
||||
fieldName = parts[1]
|
||||
collection = collections[parts[0]]
|
||||
isMainTableField = parts[0] == mainTable.alias
|
||||
} else {
|
||||
fieldName = parts[0]
|
||||
collection = collections[mainTable.alias]
|
||||
isMainTableField = true
|
||||
}
|
||||
|
||||
// fallback to the default field if the found column is not from a collection schema
|
||||
@@ -316,8 +320,8 @@ func (dao *Dao) parseQueryToFields(selectQuery string) (map[string]*queryField,
|
||||
continue
|
||||
}
|
||||
|
||||
if fieldName == schema.FieldNameId && !isMainTableField {
|
||||
// convert to relation since it is a direct id reference to non-maintable collection
|
||||
if fieldName == schema.FieldNameId {
|
||||
// convert to relation since it is a direct id reference
|
||||
result[col.alias] = &queryField{
|
||||
field: &schema.SchemaField{
|
||||
Name: col.alias,
|
||||
|
||||
+47
-4
@@ -5,12 +5,33 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
)
|
||||
|
||||
func ensureNoTempViews(app core.App, t *testing.T) {
|
||||
var total int
|
||||
|
||||
err := app.Dao().DB().Select("count(*)").
|
||||
From("sqlite_schema").
|
||||
AndWhere(dbx.HashExp{"type": "view"}).
|
||||
AndWhere(dbx.NewExp(`[[name]] LIKE '%\_temp\_%' ESCAPE '\'`)).
|
||||
Limit(1).
|
||||
Row(&total)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check for temp views: %v", err)
|
||||
}
|
||||
|
||||
if total > 0 {
|
||||
t.Fatalf("Expected all temp views to be deleted, got %d", total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteView(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
@@ -34,6 +55,8 @@ func TestDeleteView(t *testing.T) {
|
||||
t.Errorf("[%d - %q] Expected hasErr %v, got %v (%v)", i, s.viewName, s.expectError, hasErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
ensureNoTempViews(app, t)
|
||||
}
|
||||
|
||||
func TestSaveView(t *testing.T) {
|
||||
@@ -78,7 +101,7 @@ func TestSaveView(t *testing.T) {
|
||||
{
|
||||
"missing table",
|
||||
"123Test",
|
||||
"select * from missing",
|
||||
"select id from missing",
|
||||
true,
|
||||
nil,
|
||||
},
|
||||
@@ -136,7 +159,7 @@ func TestSaveView(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
infoRows, err := app.Dao().GetTableInfo(s.viewName)
|
||||
infoRows, err := app.Dao().TableInfo(s.viewName)
|
||||
if err != nil {
|
||||
t.Errorf("[%s] Failed to fetch table info for %s: %v", s.scenarioName, s.viewName, err)
|
||||
continue
|
||||
@@ -153,6 +176,24 @@ func TestSaveView(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ensureNoTempViews(app, t)
|
||||
}
|
||||
|
||||
func TestCreateViewSchemaWithDiscardedNestedTransaction(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
|
||||
_, err := txDao.CreateViewSchema("select id from missing")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
ensureNoTempViews(app, t)
|
||||
}
|
||||
|
||||
func TestCreateViewSchema(t *testing.T) {
|
||||
@@ -179,7 +220,7 @@ func TestCreateViewSchema(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"missing table",
|
||||
"select * from missing",
|
||||
"select id from missing",
|
||||
true,
|
||||
nil,
|
||||
},
|
||||
@@ -358,7 +399,7 @@ func TestCreateViewSchema(t *testing.T) {
|
||||
`,
|
||||
false,
|
||||
map[string]string{
|
||||
"id2": schema.FieldTypeJson,
|
||||
"id2": schema.FieldTypeRelation,
|
||||
"text_alias": schema.FieldTypeText,
|
||||
"url_alias": schema.FieldTypeUrl,
|
||||
"bool_alias": schema.FieldTypeBool,
|
||||
@@ -403,6 +444,8 @@ func TestCreateViewSchema(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ensureNoTempViews(app, t)
|
||||
}
|
||||
|
||||
func TestFindRecordByViewFile(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package forms
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
var privateKeyRegex = regexp.MustCompile(`(?m)-----BEGIN PRIVATE KEY----[\s\S]+-----END PRIVATE KEY-----`)
|
||||
|
||||
// AppleClientSecretCreate is a [models.Admin] upsert (create/update) form.
|
||||
//
|
||||
// Reference: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
|
||||
type AppleClientSecretCreate struct {
|
||||
app core.App
|
||||
|
||||
// ClientId is the identifier of your app (aka. Service ID).
|
||||
ClientId string `form:"clientId" json:"clientId"`
|
||||
|
||||
// TeamId is a 10-character string associated with your developer account
|
||||
// (usually could be found next to your name in the Apple Developer site).
|
||||
TeamId string `form:"teamId" json:"teamId"`
|
||||
|
||||
// KeyId is a 10-character key identifier generated for the "Sign in with Apple"
|
||||
// private key associated with your developer account.
|
||||
KeyId string `form:"keyId" json:"keyId"`
|
||||
|
||||
// PrivateKey is the private key associated to your app.
|
||||
// Usually wrapped within -----BEGIN PRIVATE KEY----- X -----END PRIVATE KEY-----.
|
||||
PrivateKey string `form:"privateKey" json:"privateKey"`
|
||||
|
||||
// Duration specifies how long the generated JWT token should be considered valid.
|
||||
// The specified value must be in seconds and max 15777000 (~6months).
|
||||
Duration int `form:"duration" json:"duration"`
|
||||
}
|
||||
|
||||
// NewAppleClientSecretCreate creates a new [AppleClientSecretCreate] form with initializer
|
||||
// config created from the provided [core.App] instances.
|
||||
func NewAppleClientSecretCreate(app core.App) *AppleClientSecretCreate {
|
||||
form := &AppleClientSecretCreate{
|
||||
app: app,
|
||||
}
|
||||
|
||||
return form
|
||||
}
|
||||
|
||||
// Validate makes the form validatable by implementing [validation.Validatable] interface.
|
||||
func (form *AppleClientSecretCreate) Validate() error {
|
||||
return validation.ValidateStruct(form,
|
||||
validation.Field(&form.ClientId, validation.Required),
|
||||
validation.Field(&form.TeamId, validation.Required, validation.Length(10, 10)),
|
||||
validation.Field(&form.KeyId, validation.Required, validation.Length(10, 10)),
|
||||
validation.Field(&form.PrivateKey, validation.Required, validation.Match(privateKeyRegex)),
|
||||
validation.Field(&form.Duration, validation.Required, validation.Min(1), validation.Max(15777000)),
|
||||
)
|
||||
}
|
||||
|
||||
// Submit validates the form and returns a new Apple Client Secret JWT.
|
||||
func (form *AppleClientSecretCreate) Submit() (string, error) {
|
||||
if err := form.Validate(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signKey, err := jwt.ParseECPrivateKeyFromPEM([]byte(strings.TrimSpace(form.PrivateKey)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
claims := &jwt.RegisteredClaims{
|
||||
Audience: jwt.ClaimStrings{"https://appleid.apple.com"},
|
||||
Subject: form.ClientId,
|
||||
Issuer: form.TeamId,
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(form.Duration) * time.Second)),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
|
||||
token.Header["kid"] = form.KeyId
|
||||
|
||||
return token.SignedString(signKey)
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package forms_test
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/pocketbase/pocketbase/forms"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
)
|
||||
|
||||
func TestAppleClientSecretCreateValidateAndSubmit(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
encodedKey, err := x509.MarshalPKCS8PrivateKey(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
privatePem := pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: encodedKey,
|
||||
},
|
||||
)
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
formData map[string]any
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
"empty data",
|
||||
map[string]any{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"invalid data",
|
||||
map[string]any{
|
||||
"clientId": "",
|
||||
"teamId": "123456789",
|
||||
"keyId": "123456789",
|
||||
"privateKey": "-----BEGIN PRIVATE KEY----- invalid -----END PRIVATE KEY-----",
|
||||
"duration": -1,
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"valid data",
|
||||
map[string]any{
|
||||
"clientId": "123",
|
||||
"teamId": "1234567890",
|
||||
"keyId": "1234567891",
|
||||
"privateKey": string(privatePem),
|
||||
"duration": 1,
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
form := forms.NewAppleClientSecretCreate(app)
|
||||
|
||||
rawData, marshalErr := json.Marshal(s.formData)
|
||||
if marshalErr != nil {
|
||||
t.Errorf("[%s] Failed to marshalize the scenario data: %v", s.name, marshalErr)
|
||||
continue
|
||||
}
|
||||
|
||||
// load data
|
||||
loadErr := json.Unmarshal(rawData, form)
|
||||
if loadErr != nil {
|
||||
t.Errorf("[%s] Failed to load form data: %v", s.name, loadErr)
|
||||
continue
|
||||
}
|
||||
|
||||
secret, err := form.Submit()
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, err)
|
||||
}
|
||||
|
||||
if hasErr {
|
||||
continue
|
||||
}
|
||||
|
||||
if secret == "" {
|
||||
t.Errorf("[%s] Expected non-empty secret", s.name)
|
||||
}
|
||||
|
||||
claims := jwt.MapClaims{}
|
||||
token, _, err := jwt.NewParser().ParseUnverified(secret, claims)
|
||||
if err != nil {
|
||||
t.Errorf("[%s] Failed to parse token: %v", s.name, err)
|
||||
}
|
||||
|
||||
if alg := token.Header["alg"]; alg != "ES256" {
|
||||
t.Errorf("[%s] Expected %q alg header, got %q", s.name, "ES256", alg)
|
||||
}
|
||||
|
||||
if kid := token.Header["kid"]; kid != form.KeyId {
|
||||
t.Errorf("[%s] Expected %q kid header, got %q", s.name, form.KeyId, kid)
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
-18
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"strconv"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
"github.com/pocketbase/pocketbase/resolvers"
|
||||
"github.com/pocketbase/pocketbase/tools/dbutils"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
"github.com/pocketbase/pocketbase/tools/search"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
@@ -26,17 +27,18 @@ type CollectionUpsert struct {
|
||||
dao *daos.Dao
|
||||
collection *models.Collection
|
||||
|
||||
Id string `form:"id" json:"id"`
|
||||
Type string `form:"type" json:"type"`
|
||||
Name string `form:"name" json:"name"`
|
||||
System bool `form:"system" json:"system"`
|
||||
Schema schema.Schema `form:"schema" json:"schema"`
|
||||
ListRule *string `form:"listRule" json:"listRule"`
|
||||
ViewRule *string `form:"viewRule" json:"viewRule"`
|
||||
CreateRule *string `form:"createRule" json:"createRule"`
|
||||
UpdateRule *string `form:"updateRule" json:"updateRule"`
|
||||
DeleteRule *string `form:"deleteRule" json:"deleteRule"`
|
||||
Options types.JsonMap `form:"options" json:"options"`
|
||||
Id string `form:"id" json:"id"`
|
||||
Type string `form:"type" json:"type"`
|
||||
Name string `form:"name" json:"name"`
|
||||
System bool `form:"system" json:"system"`
|
||||
Schema schema.Schema `form:"schema" json:"schema"`
|
||||
Indexes types.JsonArray[string] `form:"indexes" json:"indexes"`
|
||||
ListRule *string `form:"listRule" json:"listRule"`
|
||||
ViewRule *string `form:"viewRule" json:"viewRule"`
|
||||
CreateRule *string `form:"createRule" json:"createRule"`
|
||||
UpdateRule *string `form:"updateRule" json:"updateRule"`
|
||||
DeleteRule *string `form:"deleteRule" json:"deleteRule"`
|
||||
Options types.JsonMap `form:"options" json:"options"`
|
||||
}
|
||||
|
||||
// NewCollectionUpsert creates a new [CollectionUpsert] form with initializer
|
||||
@@ -57,6 +59,7 @@ func NewCollectionUpsert(app core.App, collection *models.Collection) *Collectio
|
||||
form.Type = form.collection.Type
|
||||
form.Name = form.collection.Name
|
||||
form.System = form.collection.System
|
||||
form.Indexes = form.collection.Indexes
|
||||
form.ListRule = form.collection.ListRule
|
||||
form.ViewRule = form.collection.ViewRule
|
||||
form.CreateRule = form.collection.CreateRule
|
||||
@@ -155,6 +158,7 @@ func (form *CollectionUpsert) Validate() error {
|
||||
validation.When(isView, validation.Nil),
|
||||
validation.By(form.checkRule),
|
||||
),
|
||||
validation.Field(&form.Indexes, validation.By(form.checkIndexes)),
|
||||
validation.Field(&form.Options, validation.By(form.checkOptions)),
|
||||
)
|
||||
}
|
||||
@@ -172,11 +176,6 @@ func (form *CollectionUpsert) checkUniqueName(value any) error {
|
||||
return validation.NewError("validation_collection_name_id_duplicate", "The name must not match an existing collection id.")
|
||||
}
|
||||
|
||||
// ensure that there is no existing table name with the same name
|
||||
if (form.collection.IsNew() || !strings.EqualFold(v, form.collection.Name)) && form.dao.HasTable(v) {
|
||||
return validation.NewError("validation_collection_name_table_exists", "The collection name must be also unique table name.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -386,6 +385,44 @@ func (form *CollectionUpsert) checkRule(value any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (form *CollectionUpsert) checkIndexes(value any) error {
|
||||
v, _ := value.(types.JsonArray[string])
|
||||
|
||||
if form.Type == models.CollectionTypeView && len(v) > 0 {
|
||||
return validation.NewError(
|
||||
"validation_indexes_not_supported",
|
||||
"The collection doesn't support indexes.",
|
||||
)
|
||||
}
|
||||
|
||||
for i, rawIndex := range v {
|
||||
parsed := dbutils.ParseIndex(rawIndex)
|
||||
|
||||
if !parsed.IsValid() {
|
||||
return validation.Errors{
|
||||
strconv.Itoa(i): validation.NewError(
|
||||
"validation_invalid_index_expression",
|
||||
"Invalid CREATE INDEX expression.",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// note: we don't check the index table because it is always
|
||||
// overwritten by the daos.SyncRecordTableSchema to allow
|
||||
// easier partial modifications (eg. changing only the collection name).
|
||||
// if !strings.EqualFold(parsed.TableName, form.Name) {
|
||||
// return validation.Errors{
|
||||
// strconv.Itoa(i): validation.NewError(
|
||||
// "validation_invalid_index_table",
|
||||
// fmt.Sprintf("The index table must be the same as the collection name."),
|
||||
// ),
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (form *CollectionUpsert) checkOptions(value any) error {
|
||||
v, _ := value.(types.JsonMap)
|
||||
|
||||
@@ -473,9 +510,15 @@ func (form *CollectionUpsert) Submit(interceptors ...InterceptorFunc[*models.Col
|
||||
form.collection.Name = form.Name
|
||||
}
|
||||
|
||||
// view schema is autogenerated on save
|
||||
// view schema is autogenerated on save and cannot have indexes
|
||||
if !form.collection.IsView() {
|
||||
form.collection.Schema = form.Schema
|
||||
|
||||
// normalize indexes format
|
||||
form.collection.Indexes = make(types.JsonArray[string], len(form.Indexes))
|
||||
for i, rawIdx := range form.Indexes {
|
||||
form.collection.Indexes[i] = dbutils.ParseIndex(rawIdx).Build()
|
||||
}
|
||||
}
|
||||
|
||||
form.collection.ListRule = form.ListRule
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"github.com/pocketbase/pocketbase/tools/dbutils"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
@@ -114,9 +115,10 @@ func TestCollectionUpsertValidateAndSubmit(t *testing.T) {
|
||||
"viewRule": "missing = '123'",
|
||||
"createRule": "missing = '123'",
|
||||
"updateRule": "missing = '123'",
|
||||
"deleteRule": "missing = '123'"
|
||||
"deleteRule": "missing = '123'",
|
||||
"indexes": ["create index '' on '' ()"]
|
||||
}`,
|
||||
[]string{"name", "type", "schema", "listRule", "viewRule", "createRule", "updateRule", "deleteRule"},
|
||||
[]string{"name", "type", "schema", "listRule", "viewRule", "createRule", "updateRule", "deleteRule", "indexes"},
|
||||
},
|
||||
{
|
||||
"create failure - existing name",
|
||||
@@ -234,7 +236,8 @@ func TestCollectionUpsertValidateAndSubmit(t *testing.T) {
|
||||
"viewRule": "test1='123' && emailVisibility = true",
|
||||
"createRule": "test1='123' && email != ''",
|
||||
"updateRule": "test1='123' && username != ''",
|
||||
"deleteRule": "test1='123' && id != ''"
|
||||
"deleteRule": "test1='123' && id != ''",
|
||||
"indexes": ["create index idx_test_new on anything (test1)"]
|
||||
}`,
|
||||
[]string{},
|
||||
},
|
||||
@@ -245,9 +248,10 @@ func TestCollectionUpsertValidateAndSubmit(t *testing.T) {
|
||||
"schema": [
|
||||
{"id":"a123456","name":"test1","type":"url"},
|
||||
{"id":"b123456","name":"test2","type":"bool"}
|
||||
]
|
||||
],
|
||||
"indexes": ["create index idx_test_new on test_new (test1)", "invalid"]
|
||||
}`,
|
||||
[]string{"schema"},
|
||||
[]string{"schema", "indexes"},
|
||||
},
|
||||
{
|
||||
"update success - rename fields to existing field names (aka. reusing field names)",
|
||||
@@ -326,9 +330,10 @@ func TestCollectionUpsertValidateAndSubmit(t *testing.T) {
|
||||
"createRule": "missing = '123'",
|
||||
"updateRule": "missing = '123'",
|
||||
"deleteRule": "missing = '123'",
|
||||
"options": {"test": 123}
|
||||
"options": {"test": 123},
|
||||
"indexes": ["create index '' from demo2 on (id)"]
|
||||
}`,
|
||||
[]string{"name", "type", "system", "schema", "listRule", "viewRule", "createRule", "updateRule", "deleteRule"},
|
||||
[]string{"name", "type", "system", "schema", "listRule", "viewRule", "createRule", "updateRule", "deleteRule", "indexes"},
|
||||
},
|
||||
{
|
||||
"update success - update all fields",
|
||||
@@ -344,7 +349,11 @@ func TestCollectionUpsertValidateAndSubmit(t *testing.T) {
|
||||
"createRule": "test='123' && email != ''",
|
||||
"updateRule": "test='123' && username != ''",
|
||||
"deleteRule": "test='123' && id != ''",
|
||||
"options": {"minPasswordLength": 10}
|
||||
"options": {"minPasswordLength": 10},
|
||||
"indexes": [
|
||||
"create index idx_clients_test1 on anything (id, email, test)",
|
||||
"create unique index idx_clients_test2 on clients (id, username, email)"
|
||||
]
|
||||
}`,
|
||||
[]string{},
|
||||
},
|
||||
@@ -371,7 +380,8 @@ func TestCollectionUpsertValidateAndSubmit(t *testing.T) {
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null
|
||||
"deleteRule": null,
|
||||
"indexes": []
|
||||
}`,
|
||||
[]string{},
|
||||
},
|
||||
@@ -403,12 +413,14 @@ func TestCollectionUpsertValidateAndSubmit(t *testing.T) {
|
||||
],
|
||||
"options": {
|
||||
"query": "select id, email from users; drop table _admins;"
|
||||
}
|
||||
},
|
||||
"indexes": ["create index idx_test_view on upsert_view (id)"]
|
||||
}`,
|
||||
[]string{
|
||||
"listRule",
|
||||
"viewRule",
|
||||
"options",
|
||||
"indexes", // views don't have indexes
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -592,6 +604,28 @@ func TestCollectionUpsertValidateAndSubmit(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// check indexes (if any)
|
||||
allIndexes, _ := app.Dao().TableIndexes(form.Name)
|
||||
for _, formIdx := range form.Indexes {
|
||||
parsed := dbutils.ParseIndex(formIdx)
|
||||
parsed.TableName = form.Name
|
||||
normalizedIdx := parsed.Build()
|
||||
|
||||
var exists bool
|
||||
for _, idx := range allIndexes {
|
||||
if dbutils.ParseIndex(idx).Build() == normalizedIdx {
|
||||
exists = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !exists {
|
||||
t.Errorf(
|
||||
"[%s] Missing index %s \nin \n%v", s.testName, normalizedIdx, allIndexes)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
|
||||
expectError: true,
|
||||
expectCollectionsCount: totalCollections,
|
||||
expectEvents: map[string]int{
|
||||
"OnModelBeforeDelete": 5,
|
||||
"OnModelBeforeDelete": 7,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -215,7 +215,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
|
||||
"collections": [
|
||||
{
|
||||
"id":"sz5l5z67tg7gku0",
|
||||
"name":"demo2",
|
||||
"name":"demo2_rename",
|
||||
"schema":[
|
||||
{
|
||||
"id":"_2hlxbmp",
|
||||
@@ -347,6 +347,28 @@ func TestCollectionsImportSubmit(t *testing.T) {
|
||||
"OnModelAfterDelete": totalCollections - 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "lazy system table name error",
|
||||
jsonData: `{
|
||||
"collections": [
|
||||
{
|
||||
"name": "_admins",
|
||||
"schema": [
|
||||
{
|
||||
"id":"fz6iql2m",
|
||||
"name":"active",
|
||||
"type":"bool"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`,
|
||||
expectError: true,
|
||||
expectCollectionsCount: totalCollections,
|
||||
expectEvents: map[string]int{
|
||||
"OnModelBeforeCreate": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "lazy view evaluation",
|
||||
jsonData: `{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package forms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||
@@ -16,9 +18,10 @@ import (
|
||||
|
||||
// RecordOAuth2LoginData defines the OA
|
||||
type RecordOAuth2LoginData struct {
|
||||
ExternalAuth *models.ExternalAuth
|
||||
Record *models.Record
|
||||
OAuth2User *auth.AuthUser
|
||||
ExternalAuth *models.ExternalAuth
|
||||
Record *models.Record
|
||||
OAuth2User *auth.AuthUser
|
||||
ProviderClient auth.Provider
|
||||
}
|
||||
|
||||
// BeforeOAuth2RecordCreateFunc defines a callback function that will
|
||||
@@ -127,6 +130,11 @@ func (form *RecordOAuth2Login) Submit(
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
provider.SetContext(ctx)
|
||||
|
||||
// load provider configuration
|
||||
providerConfig := form.app.Settings().NamedAuthProviderConfigs()[form.Provider]
|
||||
if err := providerConfig.SetupProvider(provider); err != nil {
|
||||
@@ -169,9 +177,10 @@ func (form *RecordOAuth2Login) Submit(
|
||||
}
|
||||
|
||||
interceptorData := &RecordOAuth2LoginData{
|
||||
ExternalAuth: rel,
|
||||
Record: authRecord,
|
||||
OAuth2User: authUser,
|
||||
ExternalAuth: rel,
|
||||
Record: authRecord,
|
||||
OAuth2User: authUser,
|
||||
ProviderClient: provider,
|
||||
}
|
||||
|
||||
interceptorsErr := runInterceptors(interceptorData, func(newData *RecordOAuth2LoginData) error {
|
||||
|
||||
+50
-2
@@ -728,6 +728,8 @@ func (form *RecordUpsert) DrySubmit(callback func(txDao *daos.Dao) error) error
|
||||
// You can optionally provide a list of InterceptorFunc to further
|
||||
// modify the form behavior before persisting it.
|
||||
func (form *RecordUpsert) Submit(interceptors ...InterceptorFunc[*models.Record]) error {
|
||||
oldRecord := form.record.OriginalCopy()
|
||||
|
||||
if err := form.ValidateAndFill(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -741,12 +743,31 @@ func (form *RecordUpsert) Submit(interceptors ...InterceptorFunc[*models.Record]
|
||||
}
|
||||
|
||||
// persist the record model
|
||||
if saveErr := form.dao.SaveRecord(form.record); saveErr != nil {
|
||||
return fmt.Errorf("failed to save the record: %w", saveErr)
|
||||
if err := form.dao.SaveRecord(form.record); err != nil {
|
||||
preparedErr := form.prepareError(err)
|
||||
if _, ok := preparedErr.(validation.Errors); ok {
|
||||
return preparedErr
|
||||
}
|
||||
return fmt.Errorf("failed to save the record: %w", err)
|
||||
}
|
||||
|
||||
// upload new files (if any)
|
||||
//
|
||||
// note1: executed outside of transaction to avoid keeping a lock for too long
|
||||
// note2: executed after the record save to allow record id change with a before model hook
|
||||
if err := form.processFilesToUpload(); err != nil {
|
||||
if oldRecord.IsNew() {
|
||||
// delete previously inserted record
|
||||
if err := form.dao.DeleteRecord(form.record); err != nil && form.app.IsDebug() {
|
||||
log.Println(err)
|
||||
}
|
||||
} else {
|
||||
// revert record changes
|
||||
if err := form.dao.SaveRecord(oldRecord); err != nil && form.app.IsDebug() {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to process the uploaded files: %w", err)
|
||||
}
|
||||
|
||||
@@ -849,3 +870,30 @@ func (form *RecordUpsert) deleteFilesByNamesList(filenames []string) ([]string,
|
||||
|
||||
return filenames, nil
|
||||
}
|
||||
|
||||
// prepareError parses the provided error and tries to return
|
||||
// user-friendly validation error(s).
|
||||
func (form *RecordUpsert) prepareError(err error) error {
|
||||
msg := strings.ToLower(err.Error())
|
||||
|
||||
validationErrs := validation.Errors{}
|
||||
|
||||
// check for unique constraint failure
|
||||
if strings.Contains(msg, "unique constraint failed") {
|
||||
msg = strings.ReplaceAll(strings.TrimSpace(msg), ",", " ")
|
||||
|
||||
c := form.record.Collection()
|
||||
for _, f := range c.Schema.Fields() {
|
||||
// blank space to unify multi-columns lookup
|
||||
if strings.Contains(msg+" ", fmt.Sprintf("%s.%s ", strings.ToLower(c.Name), f.Name)) {
|
||||
validationErrs[f.Name] = validation.NewError("validation_not_unique", "Value must be unique")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(validationErrs) > 0 {
|
||||
return validationErrs
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -1037,3 +1037,51 @@ func TestRecordUpsertAddAndRemoveFiles(t *testing.T) {
|
||||
t.Fatalf("Expected file_many to be 5, got %v", fileMany)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordUpsertUploadFailure(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
collection, err := app.Dao().FindCollectionByNameOrId("demo3")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// create with invalid file
|
||||
{
|
||||
new := models.NewRecord(collection)
|
||||
new.Id = "123456789012341"
|
||||
|
||||
form := forms.NewRecordUpsert(app, new)
|
||||
form.LoadData(map[string]any{"title": "new_test"})
|
||||
form.AddFiles("files", &filesystem.File{Reader: &filesystem.PathReader{Path: "/tmp/__missing__"}})
|
||||
|
||||
if err := form.Submit(); err == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
|
||||
if r, err := app.Dao().FindRecordById(collection.Id, new.Id); err == nil {
|
||||
t.Fatalf("Expected the inserted record to be deleted, found \n%v", r.PublicExport())
|
||||
}
|
||||
}
|
||||
|
||||
// update with invalid file
|
||||
{
|
||||
record, err := app.Dao().FindRecordById(collection.Id, "1tmknxy2868d869")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
form := forms.NewRecordUpsert(app, record)
|
||||
form.LoadData(map[string]any{"title": "update_test"})
|
||||
form.AddFiles("files", &filesystem.File{Reader: &filesystem.PathReader{Path: "/tmp/__missing__"}})
|
||||
|
||||
if err := form.Submit(); err == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
|
||||
if r, _ := app.Dao().FindRecordById(collection.Id, record.Id); r == nil || r.GetString("title") == "update_test" {
|
||||
t.Fatalf("Expected the record changes to be reverted, got \n%v", r.PublicExport())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,17 +86,6 @@ func (validator *RecordDataValidator) Validate(data map[string]any) error {
|
||||
errs[key] = err
|
||||
continue
|
||||
}
|
||||
|
||||
// check unique constraint
|
||||
if field.Unique && !validator.dao.IsRecordValueUnique(
|
||||
validator.record.Collection().Id,
|
||||
key,
|
||||
value,
|
||||
validator.record.GetId(),
|
||||
) {
|
||||
errs[key] = validation.NewError("validation_not_unique", "Value must be unique")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
|
||||
@@ -99,16 +99,6 @@ func TestRecordDataValidatorValidateText(t *testing.T) {
|
||||
nil,
|
||||
[]string{"field2"},
|
||||
},
|
||||
{
|
||||
"(text) check unique constraint",
|
||||
map[string]any{
|
||||
"field1": "test",
|
||||
"field2": "test",
|
||||
"field3": "test",
|
||||
},
|
||||
nil,
|
||||
[]string{"field3"},
|
||||
},
|
||||
{
|
||||
"(text) check min constraint",
|
||||
map[string]any{
|
||||
@@ -225,16 +215,6 @@ func TestRecordDataValidatorValidateNumber(t *testing.T) {
|
||||
nil,
|
||||
[]string{"field2"},
|
||||
},
|
||||
{
|
||||
"(number) check unique constraint",
|
||||
map[string]any{
|
||||
"field1": 123,
|
||||
"field2": 123,
|
||||
"field3": 123,
|
||||
},
|
||||
nil,
|
||||
[]string{"field3"},
|
||||
},
|
||||
{
|
||||
"(number) check min constraint",
|
||||
map[string]any{
|
||||
@@ -345,16 +325,6 @@ func TestRecordDataValidatorValidateBool(t *testing.T) {
|
||||
nil,
|
||||
[]string{"field2"},
|
||||
},
|
||||
{
|
||||
"(bool) check unique constraint",
|
||||
map[string]any{
|
||||
"field1": true,
|
||||
"field2": true,
|
||||
"field3": true,
|
||||
},
|
||||
nil,
|
||||
[]string{"field3"},
|
||||
},
|
||||
{
|
||||
"(bool) valid data (only required)",
|
||||
map[string]any{
|
||||
@@ -441,16 +411,6 @@ func TestRecordDataValidatorValidateEmail(t *testing.T) {
|
||||
nil,
|
||||
[]string{"field1", "field2", "field3"},
|
||||
},
|
||||
{
|
||||
"(email) check unique constraint",
|
||||
map[string]any{
|
||||
"field1": "test@example.com",
|
||||
"field2": "test@test.com",
|
||||
"field3": "test@example.com",
|
||||
},
|
||||
nil,
|
||||
[]string{"field3"},
|
||||
},
|
||||
{
|
||||
"(email) check ExceptDomains constraint",
|
||||
map[string]any{
|
||||
@@ -557,16 +517,6 @@ func TestRecordDataValidatorValidateUrl(t *testing.T) {
|
||||
nil,
|
||||
[]string{"field1", "field3"},
|
||||
},
|
||||
{
|
||||
"(url) check unique constraint",
|
||||
map[string]any{
|
||||
"field1": "http://example.com",
|
||||
"field2": "http://test.com",
|
||||
"field3": "http://example.com",
|
||||
},
|
||||
nil,
|
||||
[]string{"field3"},
|
||||
},
|
||||
{
|
||||
"(url) check ExceptDomains constraint",
|
||||
map[string]any{
|
||||
@@ -695,16 +645,6 @@ func TestRecordDataValidatorValidateDate(t *testing.T) {
|
||||
nil,
|
||||
[]string{"field2"},
|
||||
},
|
||||
{
|
||||
"(date) check unique constraint",
|
||||
map[string]any{
|
||||
"field1": "2029-01-01 01:01:01.123",
|
||||
"field2": "2029-01-01 01:01:01.123",
|
||||
"field3": "2029-01-01 01:01:01.123",
|
||||
},
|
||||
nil,
|
||||
[]string{"field3"},
|
||||
},
|
||||
{
|
||||
"(date) check min date constraint",
|
||||
map[string]any{
|
||||
@@ -827,26 +767,6 @@ func TestRecordDataValidatorValidateSelect(t *testing.T) {
|
||||
nil,
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"(select) check unique constraint",
|
||||
map[string]any{
|
||||
"field1": "a",
|
||||
"field2": "b",
|
||||
"field3": []string{"a", "b", "c"},
|
||||
},
|
||||
nil,
|
||||
[]string{"field3"},
|
||||
},
|
||||
{
|
||||
"(select) check unique constraint - same elements but different order",
|
||||
map[string]any{
|
||||
"field1": "a",
|
||||
"field2": "b",
|
||||
"field3": []string{"a", "c", "b"},
|
||||
},
|
||||
nil,
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"(select) check Values constraint",
|
||||
map[string]any{
|
||||
@@ -977,16 +897,6 @@ func TestRecordDataValidatorValidateJson(t *testing.T) {
|
||||
nil,
|
||||
[]string{"field2"},
|
||||
},
|
||||
{
|
||||
"(json) check unique constraint",
|
||||
map[string]any{
|
||||
"field1": `{"test":123}`,
|
||||
"field2": `{"test":123}`,
|
||||
"field3": map[string]any{"test": 123},
|
||||
},
|
||||
nil,
|
||||
[]string{"field3"},
|
||||
},
|
||||
{
|
||||
"(json) check json text invalid obj, array and number normalizations",
|
||||
map[string]any{
|
||||
@@ -1268,16 +1178,6 @@ func TestRecordDataValidatorValidateRelation(t *testing.T) {
|
||||
nil,
|
||||
[]string{"field3"},
|
||||
},
|
||||
{
|
||||
"check unique constraint",
|
||||
map[string]any{
|
||||
"field1": relId1,
|
||||
"field2": relId2,
|
||||
"field3": []string{relId1, relId2, relId3, relId3}, // repeating values are collapsed
|
||||
},
|
||||
nil,
|
||||
[]string{"field3"},
|
||||
},
|
||||
{
|
||||
"check nonexisting collection id",
|
||||
map[string]any{
|
||||
|
||||
@@ -4,13 +4,13 @@ go 1.18
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6
|
||||
github.com/aws/aws-sdk-go v1.44.209
|
||||
github.com/aws/aws-sdk-go v1.44.229
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/domodwyer/mailyak/v3 v3.4.0
|
||||
github.com/domodwyer/mailyak/v3 v3.5.0
|
||||
github.com/dop251/goja v0.0.0-20221118162653-d4bf6fde1b86
|
||||
github.com/dop251/goja_nodejs v0.0.0-20221009164102-3aa5028e57f6
|
||||
github.com/fatih/color v1.14.1
|
||||
github.com/gabriel-vasile/mimetype v1.4.1
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.2
|
||||
github.com/ganigeorgiev/fexpr v0.3.0
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
@@ -19,67 +19,67 @@ require (
|
||||
github.com/pocketbase/dbx v1.10.0
|
||||
github.com/spf13/cast v1.5.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
gocloud.dev v0.28.0
|
||||
golang.org/x/crypto v0.6.0
|
||||
golang.org/x/net v0.7.0
|
||||
golang.org/x/oauth2 v0.5.0
|
||||
gocloud.dev v0.29.0
|
||||
golang.org/x/crypto v0.7.0
|
||||
golang.org/x/net v0.8.0
|
||||
golang.org/x/oauth2 v0.6.0
|
||||
modernc.org/sqlite v1.21.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.5 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.55 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.59 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.31.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 // indirect
|
||||
github.com/aws/smithy-go v1.13.5 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/google/wire v0.5.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.8.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/image v0.5.0 // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/term v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
golang.org/x/image v0.6.0 // indirect
|
||||
golang.org/x/mod v0.9.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.6.0 // indirect
|
||||
golang.org/x/tools v0.7.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/api v0.110.0 // indirect
|
||||
google.golang.org/api v0.114.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 // indirect
|
||||
google.golang.org/grpc v1.53.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230323212658-478b75c54725 // indirect
|
||||
google.golang.org/grpc v1.54.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
lukechampine.com/uint128 v1.3.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.22.3 // indirect
|
||||
|
||||
+13
-45
@@ -19,12 +19,7 @@ const Layout = `
|
||||
font-family: Source Sans Pro, sans-serif, emoji;
|
||||
}
|
||||
body {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
p {
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
font-family: Source Sans Pro, sans-serif, emoji;
|
||||
padding: 20px 30px;
|
||||
}
|
||||
strong {
|
||||
font-weight: bold;
|
||||
@@ -32,6 +27,11 @@ const Layout = `
|
||||
em, i {
|
||||
font-style: italic;
|
||||
}
|
||||
p {
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
font-family: inherit;
|
||||
}
|
||||
small {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
@@ -58,54 +58,22 @@ const Layout = `
|
||||
color: #fff !important;
|
||||
background: #16161a !important;
|
||||
text-decoration: none !important;
|
||||
line-height: 45px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
line-height: 40px;
|
||||
width: auto;
|
||||
min-width: 150px;
|
||||
text-align: center;
|
||||
padding: 0 30px;
|
||||
margin: 10px 0;
|
||||
padding: 0 20px;
|
||||
margin: 5px 0;
|
||||
font-family: Source Sans Pro, sans-serif, emoji;;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
border-radius: 3px;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.wrapper {
|
||||
display: block;
|
||||
width: 460px;
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
font-family: inherit;
|
||||
}
|
||||
.content {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px 20px;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: 0px 2px 30px 0px rgb(0,0,0,0.05);
|
||||
-moz-box-shadow: 0px 2px 30px 0px rgb(0,0,0,0.05);
|
||||
box-shadow: 0px 2px 30px 0px rgb(0,0,0,0.05);
|
||||
}
|
||||
.footer {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
color: #666f75;
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<div class="content">
|
||||
{{template "content" .}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "content" .}}
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
@@ -42,8 +42,8 @@ func init() {
|
||||
[[tokenKey]] TEXT UNIQUE NOT NULL,
|
||||
[[passwordHash]] TEXT NOT NULL,
|
||||
[[lastResetSentAt]] TEXT DEFAULT "" NOT NULL,
|
||||
[[created]] TEXT DEFAULT "" NOT NULL,
|
||||
[[updated]] TEXT DEFAULT "" NOT NULL
|
||||
[[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
|
||||
[[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE {{_collections}} (
|
||||
@@ -52,14 +52,15 @@ func init() {
|
||||
[[type]] TEXT DEFAULT "base" NOT NULL,
|
||||
[[name]] TEXT UNIQUE NOT NULL,
|
||||
[[schema]] JSON DEFAULT "[]" NOT NULL,
|
||||
[[indexes]] JSON DEFAULT "[]" NOT NULL,
|
||||
[[listRule]] TEXT DEFAULT NULL,
|
||||
[[viewRule]] TEXT DEFAULT NULL,
|
||||
[[createRule]] TEXT DEFAULT NULL,
|
||||
[[updateRule]] TEXT DEFAULT NULL,
|
||||
[[deleteRule]] TEXT DEFAULT NULL,
|
||||
[[options]] JSON DEFAULT "{}" NOT NULL,
|
||||
[[created]] TEXT DEFAULT "" NOT NULL,
|
||||
[[updated]] TEXT DEFAULT "" NOT NULL
|
||||
[[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
|
||||
[[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE {{_params}} (
|
||||
@@ -76,8 +77,8 @@ func init() {
|
||||
[[recordId]] TEXT NOT NULL,
|
||||
[[provider]] TEXT NOT NULL,
|
||||
[[providerId]] TEXT NOT NULL,
|
||||
[[created]] TEXT DEFAULT "" NOT NULL,
|
||||
[[updated]] TEXT DEFAULT "" NOT NULL,
|
||||
[[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
|
||||
[[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
|
||||
---
|
||||
FOREIGN KEY ([[collectionId]]) REFERENCES {{_collections}} ([[id]]) ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
)
|
||||
|
||||
// Normalizes old single and multiple values of MultiValuer fields (file, select, relation).
|
||||
func init() {
|
||||
AppMigrations.Register(func(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
collections := []*models.Collection{}
|
||||
if err := dao.CollectionQuery().All(&collections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, c := range collections {
|
||||
if c.IsView() {
|
||||
// skip view collections
|
||||
continue
|
||||
}
|
||||
|
||||
for _, f := range c.Schema.Fields() {
|
||||
opt, ok := f.Options.(schema.MultiValuer)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var updateQuery *dbx.Query
|
||||
|
||||
if opt.IsMultiple() {
|
||||
updateQuery = dao.DB().NewQuery(fmt.Sprintf(
|
||||
`UPDATE {{%s}} set [[%s]] = (
|
||||
CASE
|
||||
WHEN COALESCE([[%s]], '') = ''
|
||||
THEN '[]'
|
||||
ELSE (
|
||||
CASE
|
||||
WHEN json_valid([[%s]]) AND json_type([[%s]]) == 'array'
|
||||
THEN [[%s]]
|
||||
ELSE json_array([[%s]])
|
||||
END
|
||||
)
|
||||
END
|
||||
)`,
|
||||
c.Name,
|
||||
f.Name,
|
||||
f.Name,
|
||||
f.Name,
|
||||
f.Name,
|
||||
f.Name,
|
||||
f.Name,
|
||||
))
|
||||
} else {
|
||||
updateQuery = dao.DB().NewQuery(fmt.Sprintf(
|
||||
`UPDATE {{%s}} set [[%s]] = (
|
||||
CASE
|
||||
WHEN COALESCE([[%s]], '[]') = '[]'
|
||||
THEN ''
|
||||
ELSE (
|
||||
CASE
|
||||
WHEN json_valid([[%s]]) AND json_type([[%s]]) == 'array'
|
||||
THEN COALESCE(json_extract([[%s]], '$[#-1]'), '')
|
||||
ELSE [[%s]]
|
||||
END
|
||||
)
|
||||
END
|
||||
)`,
|
||||
c.Name,
|
||||
f.Name,
|
||||
f.Name,
|
||||
f.Name,
|
||||
f.Name,
|
||||
f.Name,
|
||||
f.Name,
|
||||
))
|
||||
}
|
||||
|
||||
if _, err := updateQuery.Execute(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// trigger view query update after the records normalization
|
||||
// (ignore save error in case of invalid query to allow users to change it from the UI)
|
||||
for _, c := range collections {
|
||||
if !c.IsView() {
|
||||
continue
|
||||
}
|
||||
|
||||
dao.SaveCollection(c)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, func(db dbx.Builder) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/tools/dbutils"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
)
|
||||
|
||||
// Adds _collections indexes column (if not already).
|
||||
//
|
||||
// Note: This migration will be deleted once schema.SchemaField.Unuique is removed.
|
||||
func init() {
|
||||
AppMigrations.Register(func(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
// cleanup failed remaining/"dangling" temp views to prevent
|
||||
// errors during the indexes upsert
|
||||
// ---
|
||||
tempViews := []string{}
|
||||
viewsErr := db.Select("name").
|
||||
From("sqlite_schema").
|
||||
AndWhere(dbx.HashExp{"type": "view"}).
|
||||
AndWhere(dbx.NewExp(`[[name]] LIKE '\_temp\_%' ESCAPE '\'`)).
|
||||
Column(&tempViews)
|
||||
if viewsErr != nil {
|
||||
return viewsErr
|
||||
}
|
||||
for _, name := range tempViews {
|
||||
if err := dao.DeleteView(name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// ---
|
||||
|
||||
cols, err := dao.TableColumns("_collections")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, col := range cols {
|
||||
if col == "indexes" {
|
||||
return nil // already existing (probably via the init migration)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := db.AddColumn("_collections", "indexes", `JSON DEFAULT "[]" NOT NULL`).Execute(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collections := []*models.Collection{}
|
||||
if err := dao.CollectionQuery().AndWhere(dbx.NewExp("type != 'view'")).All(&collections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type indexInfo struct {
|
||||
Sql string `db:"sql"`
|
||||
IndexName string `db:"name"`
|
||||
TableName string `db:"tbl_name"`
|
||||
}
|
||||
|
||||
indexesQuery := db.NewQuery(`SELECT * FROM sqlite_master WHERE type = "index" and sql is not null`)
|
||||
rawIndexes := []indexInfo{}
|
||||
if err := indexesQuery.All(&rawIndexes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
indexesByTableName := map[string][]indexInfo{}
|
||||
for _, idx := range rawIndexes {
|
||||
indexesByTableName[idx.TableName] = append(indexesByTableName[idx.TableName], idx)
|
||||
}
|
||||
|
||||
for _, c := range collections {
|
||||
c.Indexes = nil // reset
|
||||
|
||||
excludeIndexes := []string{
|
||||
"_" + c.Id + "_email_idx",
|
||||
"_" + c.Id + "_username_idx",
|
||||
"_" + c.Id + "_tokenKey_idx",
|
||||
}
|
||||
|
||||
// convert custom indexes into the related collections
|
||||
for _, idx := range indexesByTableName[c.Name] {
|
||||
if strings.Contains(idx.IndexName, "sqlite_autoindex_") ||
|
||||
list.ExistInSlice(idx.IndexName, excludeIndexes) {
|
||||
continue
|
||||
}
|
||||
|
||||
// drop old index (it will be recreated with the collection)
|
||||
if _, err := db.DropIndex(idx.TableName, idx.IndexName).Execute(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Indexes = append(c.Indexes, idx.Sql)
|
||||
}
|
||||
|
||||
// convert unique fields to indexes
|
||||
FieldsLoop:
|
||||
for _, f := range c.Schema.Fields() {
|
||||
if !f.Unique {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, idx := range indexesByTableName[c.Name] {
|
||||
parsed := dbutils.ParseIndex(idx.Sql)
|
||||
if parsed.Unique && len(parsed.Columns) == 1 && strings.EqualFold(parsed.Columns[0].Name, f.Name) {
|
||||
continue FieldsLoop // already added
|
||||
}
|
||||
}
|
||||
|
||||
c.Indexes = append(c.Indexes, fmt.Sprintf(
|
||||
`CREATE UNIQUE INDEX "idx_unique_%s" on "%s" ("%s")`,
|
||||
f.Id,
|
||||
c.Name,
|
||||
f.Name,
|
||||
))
|
||||
}
|
||||
|
||||
if len(c.Indexes) > 0 {
|
||||
if err := dao.SaveCollection(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, func(db dbx.Builder) error {
|
||||
_, err := db.DropColumn("_collections", "indexes").Execute()
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
@@ -20,8 +20,8 @@ func init() {
|
||||
[[referer]] TEXT DEFAULT "" NOT NULL,
|
||||
[[userAgent]] TEXT DEFAULT "" NOT NULL,
|
||||
[[meta]] JSON DEFAULT "{}" NOT NULL,
|
||||
[[created]] TEXT DEFAULT "" NOT NULL,
|
||||
[[updated]] TEXT DEFAULT "" NOT NULL
|
||||
[[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
|
||||
[[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX _request_status_idx on {{_requests}} ([[status]]);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package logs
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
||||
// This migration normalizes the request logs method to UPPERCASE (eg. "get" => "GET").
|
||||
func init() {
|
||||
LogsMigrations.Register(func(db dbx.Builder) error {
|
||||
_, err := db.NewQuery("UPDATE {{_requests}} SET method=UPPER(method)").Execute()
|
||||
|
||||
return err
|
||||
}, func(db dbx.Builder) error {
|
||||
_, err := db.NewQuery("UPDATE {{_requests}} SET method=LOWER(method)").Execute()
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
+1
-1
@@ -48,7 +48,7 @@ func (m *Admin) SetPassword(password string) error {
|
||||
}
|
||||
|
||||
// hash the password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 13)
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -23,10 +23,11 @@ const (
|
||||
type Collection struct {
|
||||
BaseModel
|
||||
|
||||
Name string `db:"name" json:"name"`
|
||||
Type string `db:"type" json:"type"`
|
||||
System bool `db:"system" json:"system"`
|
||||
Schema schema.Schema `db:"schema" json:"schema"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Type string `db:"type" json:"type"`
|
||||
System bool `db:"system" json:"system"`
|
||||
Schema schema.Schema `db:"schema" json:"schema"`
|
||||
Indexes types.JsonArray[string] `db:"indexes" json:"indexes"`
|
||||
|
||||
// rules
|
||||
ListRule *string `db:"listRule" json:"listRule"`
|
||||
|
||||
@@ -75,22 +75,22 @@ func TestCollectionMarshalJSON(t *testing.T) {
|
||||
{
|
||||
"no type",
|
||||
models.Collection{Name: "test"},
|
||||
`{"id":"","created":"","updated":"","name":"test","type":"","system":false,"schema":[],"listRule":null,"viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`,
|
||||
`{"id":"","created":"","updated":"","name":"test","type":"","system":false,"schema":[],"indexes":[],"listRule":null,"viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`,
|
||||
},
|
||||
{
|
||||
"unknown type + non empty options",
|
||||
models.Collection{Name: "test", Type: "unknown", ListRule: types.Pointer("test_list"), Options: types.JsonMap{"test": 123}},
|
||||
`{"id":"","created":"","updated":"","name":"test","type":"unknown","system":false,"schema":[],"listRule":"test_list","viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`,
|
||||
models.Collection{Name: "test", Type: "unknown", ListRule: types.Pointer("test_list"), Options: types.JsonMap{"test": 123}, Indexes: types.JsonArray[string]{"idx_test"}},
|
||||
`{"id":"","created":"","updated":"","name":"test","type":"unknown","system":false,"schema":[],"indexes":["idx_test"],"listRule":"test_list","viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`,
|
||||
},
|
||||
{
|
||||
"base type + non empty options",
|
||||
models.Collection{Name: "test", Type: models.CollectionTypeBase, ListRule: types.Pointer("test_list"), Options: types.JsonMap{"test": 123}},
|
||||
`{"id":"","created":"","updated":"","name":"test","type":"base","system":false,"schema":[],"listRule":"test_list","viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`,
|
||||
`{"id":"","created":"","updated":"","name":"test","type":"base","system":false,"schema":[],"indexes":[],"listRule":"test_list","viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`,
|
||||
},
|
||||
{
|
||||
"auth type + non empty options",
|
||||
models.Collection{BaseModel: models.BaseModel{Id: "test"}, Type: models.CollectionTypeAuth, Options: types.JsonMap{"test": 123, "allowOAuth2Auth": true, "minPasswordLength": 4}},
|
||||
`{"id":"test","created":"","updated":"","name":"","type":"auth","system":false,"schema":[],"listRule":null,"viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{"allowEmailAuth":false,"allowOAuth2Auth":true,"allowUsernameAuth":false,"exceptEmailDomains":null,"manageRule":null,"minPasswordLength":4,"onlyEmailDomains":null,"requireEmail":false}}`,
|
||||
`{"id":"test","created":"","updated":"","name":"","type":"auth","system":false,"schema":[],"indexes":[],"listRule":null,"viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{"allowEmailAuth":false,"allowOAuth2Auth":true,"allowUsernameAuth":false,"exceptEmailDomains":null,"manageRule":null,"minPasswordLength":4,"onlyEmailDomains":null,"requireEmail":false}}`,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -630,16 +630,16 @@ func (m *Record) getNormalizeDataValueForDB(key string) any {
|
||||
switch ids := val.(type) {
|
||||
case []string:
|
||||
// encode string slice
|
||||
return append(types.JsonArray{}, list.ToInterfaceSlice(ids)...)
|
||||
return append(types.JsonArray[string]{}, ids...)
|
||||
case []int:
|
||||
// encode int slice
|
||||
return append(types.JsonArray{}, list.ToInterfaceSlice(ids)...)
|
||||
return append(types.JsonArray[int]{}, ids...)
|
||||
case []float64:
|
||||
// encode float64 slice
|
||||
return append(types.JsonArray{}, list.ToInterfaceSlice(ids)...)
|
||||
return append(types.JsonArray[float64]{}, ids...)
|
||||
case []any:
|
||||
// encode interface slice
|
||||
return append(types.JsonArray{}, ids...)
|
||||
return append(types.JsonArray[any]{}, ids...)
|
||||
default:
|
||||
// no changes
|
||||
return val
|
||||
@@ -864,7 +864,7 @@ func (m *Record) SetPassword(password string) error {
|
||||
}
|
||||
|
||||
// hash the password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 13)
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ type RequestData struct {
|
||||
Method string `json:"method"`
|
||||
Query map[string]any `json:"query"`
|
||||
Data map[string]any `json:"data"`
|
||||
Headers map[string]any `json:"headers"`
|
||||
AuthRecord *Record `json:"authRecord"`
|
||||
Admin *Admin `json:"admin"`
|
||||
}
|
||||
|
||||
@@ -131,8 +131,12 @@ type SchemaField struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Type string `form:"type" json:"type"`
|
||||
Required bool `form:"required" json:"required"`
|
||||
Unique bool `form:"unique" json:"unique"`
|
||||
Options any `form:"options" json:"options"`
|
||||
|
||||
// Deprecated: This field is no-op and will be removed in future versions.
|
||||
// Please use the collection.Indexes field to define a unique constraint.
|
||||
Unique bool `form:"unique" json:"unique"`
|
||||
|
||||
Options any `form:"options" json:"options"`
|
||||
}
|
||||
|
||||
// ColDefinition returns the field db column type definition as string.
|
||||
@@ -321,7 +325,7 @@ func (f *SchemaField) PrepareValue(value any) any {
|
||||
val := list.ToUniqueStringSlice(value)
|
||||
|
||||
options, _ := f.Options.(*SelectOptions)
|
||||
if options.MaxSelect <= 1 {
|
||||
if !options.IsMultiple() {
|
||||
if len(val) > 0 {
|
||||
return val[len(val)-1] // the last selected
|
||||
}
|
||||
@@ -333,7 +337,7 @@ func (f *SchemaField) PrepareValue(value any) any {
|
||||
val := list.ToUniqueStringSlice(value)
|
||||
|
||||
options, _ := f.Options.(*FileOptions)
|
||||
if options.MaxSelect <= 1 {
|
||||
if !options.IsMultiple() {
|
||||
if len(val) > 0 {
|
||||
return val[len(val)-1] // the last selected
|
||||
}
|
||||
@@ -345,7 +349,7 @@ func (f *SchemaField) PrepareValue(value any) any {
|
||||
ids := list.ToUniqueStringSlice(value)
|
||||
|
||||
options, _ := f.Options.(*RelationOptions)
|
||||
if options.MaxSelect != nil && *options.MaxSelect <= 1 {
|
||||
if !options.IsMultiple() {
|
||||
if len(ids) > 0 {
|
||||
return ids[len(ids)-1] // the last selected
|
||||
}
|
||||
@@ -399,7 +403,12 @@ func (f *SchemaField) PrepareValueWithModifier(baseValue any, modifier string, m
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// FieldOptions interfaces that defines common methods that every field options struct has.
|
||||
// MultiValuer defines common interface methods that every multi-valued (eg. with MaxSelect) field option struct has.
|
||||
type MultiValuer interface {
|
||||
IsMultiple() bool
|
||||
}
|
||||
|
||||
// FieldOptions defines common interface methods that every field option struct has.
|
||||
type FieldOptions interface {
|
||||
Validate() error
|
||||
}
|
||||
@@ -564,6 +573,12 @@ func (o SelectOptions) Validate() error {
|
||||
)
|
||||
}
|
||||
|
||||
// IsMultiple implements MultiValuer interface and checks whether the
|
||||
// current field options support multiple values.
|
||||
func (o SelectOptions) IsMultiple() bool {
|
||||
return o.MaxSelect > 1
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type JsonOptions struct {
|
||||
@@ -575,6 +590,8 @@ func (o JsonOptions) Validate() error {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
var _ MultiValuer = (*FileOptions)(nil)
|
||||
|
||||
type FileOptions struct {
|
||||
MaxSelect int `form:"maxSelect" json:"maxSelect"`
|
||||
MaxSize int `form:"maxSize" json:"maxSize"` // in bytes
|
||||
@@ -593,8 +610,16 @@ func (o FileOptions) Validate() error {
|
||||
)
|
||||
}
|
||||
|
||||
// IsMultiple implements MultiValuer interface and checks whether the
|
||||
// current field options support multiple values.
|
||||
func (o FileOptions) IsMultiple() bool {
|
||||
return o.MaxSelect > 1
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
var _ MultiValuer = (*RelationOptions)(nil)
|
||||
|
||||
type RelationOptions struct {
|
||||
// CollectionId is the id of the related collection.
|
||||
CollectionId string `form:"collectionId" json:"collectionId"`
|
||||
@@ -632,6 +657,12 @@ func (o RelationOptions) Validate() error {
|
||||
)
|
||||
}
|
||||
|
||||
// IsMultiple implements MultiValuer interface and checks whether the
|
||||
// current field options support multiple values.
|
||||
func (o RelationOptions) IsMultiple() bool {
|
||||
return o.MaxSelect == nil || *o.MaxSelect > 1
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Deprecated: Will be removed in v0.9+
|
||||
|
||||
@@ -1983,6 +1983,28 @@ func TestSelectOptionsValidate(t *testing.T) {
|
||||
checkFieldOptionsScenarios(t, scenarios)
|
||||
}
|
||||
|
||||
func TestSelectOptionsIsMultiple(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
maxSelect int
|
||||
expect bool
|
||||
}{
|
||||
{-1, false},
|
||||
{0, false},
|
||||
{1, false},
|
||||
{2, true},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
opt := schema.SelectOptions{
|
||||
MaxSelect: s.maxSelect,
|
||||
}
|
||||
|
||||
if v := opt.IsMultiple(); v != s.expect {
|
||||
t.Errorf("[%d] Expected %v, got %v", i, s.expect, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonOptionsValidate(t *testing.T) {
|
||||
scenarios := []fieldOptionsScenario{
|
||||
{
|
||||
@@ -2053,6 +2075,28 @@ func TestFileOptionsValidate(t *testing.T) {
|
||||
checkFieldOptionsScenarios(t, scenarios)
|
||||
}
|
||||
|
||||
func TestFileOptionsIsMultiple(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
maxSelect int
|
||||
expect bool
|
||||
}{
|
||||
{-1, false},
|
||||
{0, false},
|
||||
{1, false},
|
||||
{2, true},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
opt := schema.FileOptions{
|
||||
MaxSelect: s.maxSelect,
|
||||
}
|
||||
|
||||
if v := opt.IsMultiple(); v != s.expect {
|
||||
t.Errorf("[%d] Expected %v, got %v", i, s.expect, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRelationOptionsValidate(t *testing.T) {
|
||||
scenarios := []fieldOptionsScenario{
|
||||
{
|
||||
@@ -2088,3 +2132,26 @@ func TestRelationOptionsValidate(t *testing.T) {
|
||||
|
||||
checkFieldOptionsScenarios(t, scenarios)
|
||||
}
|
||||
|
||||
func TestRelationOptionsIsMultiple(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
maxSelect *int
|
||||
expect bool
|
||||
}{
|
||||
{nil, true},
|
||||
{types.Pointer(-1), false},
|
||||
{types.Pointer(0), false},
|
||||
{types.Pointer(1), false},
|
||||
{types.Pointer(2), true},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
opt := schema.RelationOptions{
|
||||
MaxSelect: s.maxSelect,
|
||||
}
|
||||
|
||||
if v := opt.IsMultiple(); v != s.expect {
|
||||
t.Errorf("[%d] Expected %v, got %v", i, s.expect, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ type Settings struct {
|
||||
OIDCAuth AuthProviderConfig `form:"oidcAuth" json:"oidcAuth"`
|
||||
OIDC2Auth AuthProviderConfig `form:"oidc2Auth" json:"oidc2Auth"`
|
||||
OIDC3Auth AuthProviderConfig `form:"oidc3Auth" json:"oidc3Auth"`
|
||||
AppleAuth AuthProviderConfig `form:"appleAuth" json:"appleAuth"`
|
||||
}
|
||||
|
||||
// New creates and returns a new default Settings instance.
|
||||
@@ -156,6 +157,9 @@ func New() *Settings {
|
||||
OIDC3Auth: AuthProviderConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
AppleAuth: AuthProviderConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +196,7 @@ func (s *Settings) Validate() error {
|
||||
validation.Field(&s.OIDCAuth),
|
||||
validation.Field(&s.OIDC2Auth),
|
||||
validation.Field(&s.OIDC3Auth),
|
||||
validation.Field(&s.AppleAuth),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -251,6 +256,7 @@ func (s *Settings) RedactClone() (*Settings, error) {
|
||||
&clone.OIDCAuth.ClientSecret,
|
||||
&clone.OIDC2Auth.ClientSecret,
|
||||
&clone.OIDC3Auth.ClientSecret,
|
||||
&clone.AppleAuth.ClientSecret,
|
||||
}
|
||||
|
||||
// mask all sensitive fields
|
||||
@@ -287,6 +293,7 @@ func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig {
|
||||
auth.NameOIDC: s.OIDCAuth,
|
||||
auth.NameOIDC + "2": s.OIDC2Auth,
|
||||
auth.NameOIDC + "3": s.OIDC3Auth,
|
||||
auth.NameApple: s.AppleAuth,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ func TestSettingsValidate(t *testing.T) {
|
||||
s.OIDC2Auth.ClientId = ""
|
||||
s.OIDC3Auth.Enabled = true
|
||||
s.OIDC3Auth.ClientId = ""
|
||||
s.AppleAuth.Enabled = true
|
||||
s.AppleAuth.ClientId = ""
|
||||
|
||||
// check if Validate() is triggering the members validate methods.
|
||||
err := s.Validate()
|
||||
@@ -98,6 +100,7 @@ func TestSettingsValidate(t *testing.T) {
|
||||
`"oidcAuth":{`,
|
||||
`"oidc2Auth":{`,
|
||||
`"oidc3Auth":{`,
|
||||
`"appleAuth":{`,
|
||||
}
|
||||
|
||||
errBytes, _ := json.Marshal(err)
|
||||
@@ -160,6 +163,8 @@ func TestSettingsMerge(t *testing.T) {
|
||||
s2.OIDC2Auth.ClientId = "oidc2_test"
|
||||
s2.OIDC3Auth.Enabled = true
|
||||
s2.OIDC3Auth.ClientId = "oidc3_test"
|
||||
s2.AppleAuth.Enabled = true
|
||||
s2.AppleAuth.ClientId = "apple_test"
|
||||
|
||||
if err := s1.Merge(s2); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -243,6 +248,7 @@ func TestSettingsRedactClone(t *testing.T) {
|
||||
s1.OIDCAuth.ClientSecret = testSecret
|
||||
s1.OIDC2Auth.ClientSecret = testSecret
|
||||
s1.OIDC3Auth.ClientSecret = testSecret
|
||||
s1.AppleAuth.ClientSecret = testSecret
|
||||
|
||||
s1Bytes, err := json.Marshal(s1)
|
||||
if err != nil {
|
||||
@@ -297,6 +303,7 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
|
||||
s.OIDCAuth.ClientId = "oidc_test"
|
||||
s.OIDC2Auth.ClientId = "oidc2_test"
|
||||
s.OIDC3Auth.ClientId = "oidc3_test"
|
||||
s.AppleAuth.ClientId = "apple_test"
|
||||
|
||||
result := s.NamedAuthProviderConfigs()
|
||||
|
||||
@@ -324,6 +331,7 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
|
||||
`"oidc":{"enabled":false,"clientId":"oidc_test"`,
|
||||
`"oidc2":{"enabled":false,"clientId":"oidc2_test"`,
|
||||
`"oidc3":{"enabled":false,"clientId":"oidc3_test"`,
|
||||
`"apple":{"enabled":false,"clientId":"apple_test"`,
|
||||
}
|
||||
for _, p := range expectedParts {
|
||||
if !strings.Contains(encodedStr, p) {
|
||||
|
||||
@@ -123,6 +123,7 @@ func (p *plugin) createCommand() *cobra.Command {
|
||||
- down [number] - reverts the last [number] applied migrations
|
||||
- create name - creates new blank migration template file
|
||||
- collections - creates new migration file with snapshot of the local collections configuration
|
||||
- history-sync - ensures that the _migrations history table doesn't have references to deleted migration files
|
||||
`
|
||||
|
||||
command := &cobra.Command{
|
||||
|
||||
@@ -31,6 +31,9 @@ migrate((db) => {
|
||||
"type": "auth",
|
||||
"system": true,
|
||||
"schema": [],
|
||||
"indexes": [
|
||||
"create index test on new_name (id)"
|
||||
],
|
||||
"listRule": "@request.auth.id != '' && created > 0 || 'backtick` + "`" + `test' = 0",
|
||||
"viewRule": "id = \"1\"",
|
||||
"createRule": null,
|
||||
@@ -81,6 +84,9 @@ func init() {
|
||||
"type": "auth",
|
||||
"system": true,
|
||||
"schema": [],
|
||||
"indexes": [
|
||||
"create index test on new_name (id)"
|
||||
],
|
||||
"listRule": "@request.auth.id != '' && created > 0 || ` + "'backtick` + \"`\" + `test' = 0" + `",
|
||||
"viewRule": "id = \"1\"",
|
||||
"createRule": null,
|
||||
@@ -143,6 +149,7 @@ func init() {
|
||||
collection.Updated = collection.Created
|
||||
collection.ListRule = types.Pointer("@request.auth.id != '' && created > 0 || 'backtick`test' = 0")
|
||||
collection.ViewRule = types.Pointer(`id = "1"`)
|
||||
collection.Indexes = types.JsonArray[string]{"create index test on new_name (id)"}
|
||||
collection.SetOptions(models.CollectionAuthOptions{
|
||||
ManageRule: types.Pointer("created > 0"),
|
||||
MinPasswordLength: 20,
|
||||
@@ -201,6 +208,9 @@ migrate((db) => {
|
||||
"type": "auth",
|
||||
"system": false,
|
||||
"schema": [],
|
||||
"indexes": [
|
||||
"create index test on test456 (id)"
|
||||
],
|
||||
"listRule": "@request.auth.id != '' && created > 0 || 'backtick` + "`" + `test' = 0",
|
||||
"viewRule": "id = \"1\"",
|
||||
"createRule": null,
|
||||
@@ -255,6 +265,9 @@ func init() {
|
||||
"type": "auth",
|
||||
"system": false,
|
||||
"schema": [],
|
||||
"indexes": [
|
||||
"create index test on test456 (id)"
|
||||
],
|
||||
"listRule": "@request.auth.id != '' && created > 0 || ` + "'backtick` + \"`\" + `test' = 0" + `",
|
||||
"viewRule": "id = \"1\"",
|
||||
"createRule": null,
|
||||
@@ -305,6 +318,7 @@ func init() {
|
||||
collection.Updated = collection.Created
|
||||
collection.ListRule = types.Pointer("@request.auth.id != '' && created > 0 || 'backtick`test' = 0")
|
||||
collection.ViewRule = types.Pointer(`id = "1"`)
|
||||
collection.Indexes = types.JsonArray[string]{"create index test on test456 (id)"}
|
||||
collection.SetOptions(models.CollectionAuthOptions{
|
||||
ManageRule: types.Pointer("created > 0"),
|
||||
MinPasswordLength: 20,
|
||||
@@ -367,6 +381,9 @@ migrate((db) => {
|
||||
collection.listRule = null
|
||||
collection.deleteRule = "updated > 0 && @request.auth.id != ''"
|
||||
collection.options = {}
|
||||
collection.indexes = [
|
||||
"create index test1 on test456_update (f1_name)"
|
||||
]
|
||||
|
||||
// remove
|
||||
collection.schema.removeField("f3_id")
|
||||
@@ -419,6 +436,9 @@ migrate((db) => {
|
||||
"onlyEmailDomains": null,
|
||||
"requireEmail": false
|
||||
}
|
||||
collection.indexes = [
|
||||
"create index test1 on test456 (f1_name)"
|
||||
]
|
||||
|
||||
// add
|
||||
collection.schema.addField(new SchemaField({
|
||||
@@ -488,6 +508,10 @@ func init() {
|
||||
json.Unmarshal([]byte(` + "`" + `{}` + "`" + `), &options)
|
||||
collection.SetOptions(options)
|
||||
|
||||
json.Unmarshal([]byte(` + "`" + `[
|
||||
"create index test1 on test456_update (f1_name)"
|
||||
]` + "`" + `), &collection.Indexes)
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("f3_id")
|
||||
|
||||
@@ -554,6 +578,10 @@ func init() {
|
||||
}` + "`" + `), &options)
|
||||
collection.SetOptions(options)
|
||||
|
||||
json.Unmarshal([]byte(` + "`" + `[
|
||||
"create index test1 on test456 (f1_name)"
|
||||
]` + "`" + `), &collection.Indexes)
|
||||
|
||||
// add
|
||||
del_f3_name := &schema.SchemaField{}
|
||||
json.Unmarshal([]byte(` + "`" + `{
|
||||
@@ -614,6 +642,7 @@ func init() {
|
||||
collection.Updated = collection.Created
|
||||
collection.ListRule = types.Pointer("@request.auth.id != '' && created > 0")
|
||||
collection.ViewRule = types.Pointer(`id = "1"`)
|
||||
collection.Indexes = types.JsonArray[string]{"create index test1 on test456 (f1_name)"}
|
||||
collection.SetOptions(models.CollectionAuthOptions{
|
||||
ManageRule: types.Pointer("created > 0"),
|
||||
MinPasswordLength: 20,
|
||||
@@ -652,6 +681,9 @@ func init() {
|
||||
collection.Type = models.CollectionTypeBase
|
||||
collection.DeleteRule = types.Pointer(`updated > 0 && @request.auth.id != ''`)
|
||||
collection.ListRule = nil
|
||||
collection.Indexes = types.JsonArray[string]{
|
||||
"create index test1 on test456_update (f1_name)",
|
||||
}
|
||||
collection.NormalizeOptions()
|
||||
collection.Schema.RemoveField("f3_id")
|
||||
collection.Schema.AddField(&schema.SchemaField{
|
||||
|
||||
@@ -196,6 +196,20 @@ func (p *plugin) jsDiffTemplate(new *models.Collection, old *models.Collection)
|
||||
downParts = append(downParts, fmt.Sprintf("%s.options = %s", varName, rawOldOptions))
|
||||
}
|
||||
|
||||
// Indexes
|
||||
rawNewIndexes, err := marhshalWithoutEscape(new.Indexes, " ", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rawOldIndexes, err := marhshalWithoutEscape(old.Indexes, " ", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !bytes.Equal(rawNewIndexes, rawOldIndexes) {
|
||||
upParts = append(upParts, fmt.Sprintf("%s.indexes = %s", varName, rawNewIndexes))
|
||||
downParts = append(downParts, fmt.Sprintf("%s.indexes = %s", varName, rawOldIndexes))
|
||||
}
|
||||
|
||||
// ensure new line between regular and collection fields
|
||||
if len(upParts) > 0 {
|
||||
upParts[len(upParts)-1] += "\n"
|
||||
@@ -571,6 +585,21 @@ func (p *plugin) goDiffTemplate(new *models.Collection, old *models.Collection)
|
||||
downParts = append(downParts, fmt.Sprintf("%s.SetOptions(options)\n", varName))
|
||||
}
|
||||
|
||||
// Indexes
|
||||
rawNewIndexes, err := marhshalWithoutEscape(new.Indexes, "\t\t", "\t")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rawOldIndexes, err := marhshalWithoutEscape(old.Indexes, "\t\t", "\t")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !bytes.Equal(rawNewIndexes, rawOldIndexes) {
|
||||
upParts = append(upParts, fmt.Sprintf("json.Unmarshal([]byte(`%s`), &%s.Indexes)\n", escapeBacktick(string(rawNewIndexes)), varName))
|
||||
// ---
|
||||
downParts = append(downParts, fmt.Sprintf("json.Unmarshal([]byte(`%s`), &%s.Indexes)\n", escapeBacktick(string(rawOldIndexes)), varName))
|
||||
}
|
||||
|
||||
// Schema
|
||||
// ---------------------------------------------------------------
|
||||
// deleted fields
|
||||
|
||||
@@ -512,44 +512,65 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
|
||||
}
|
||||
|
||||
cleanFieldName := inflector.Columnify(field.Name)
|
||||
newCollectionName := relCollection.Name
|
||||
prefixedFieldName := r.activeTableAlias + "." + cleanFieldName
|
||||
newTableAlias := r.activeTableAlias + "_" + cleanFieldName
|
||||
newCollectionName := relCollection.Name
|
||||
|
||||
if !options.IsMultiple() {
|
||||
r.resolver.registerJoin(
|
||||
inflector.Columnify(newCollectionName),
|
||||
newTableAlias,
|
||||
dbx.NewExp(fmt.Sprintf("[[%s.id]] = [[%s]]", newTableAlias, prefixedFieldName)),
|
||||
)
|
||||
} else {
|
||||
jeAlias := r.activeTableAlias + "_" + cleanFieldName + "_je"
|
||||
r.resolver.registerJoin(jsonEach(prefixedFieldName), jeAlias, nil)
|
||||
r.resolver.registerJoin(
|
||||
inflector.Columnify(newCollectionName),
|
||||
newTableAlias,
|
||||
dbx.NewExp(fmt.Sprintf("[[%s.id]] = [[%s.value]]", newTableAlias, jeAlias)),
|
||||
)
|
||||
}
|
||||
|
||||
jeAlias := r.activeTableAlias + "_" + cleanFieldName + "_je"
|
||||
jePair := r.activeTableAlias + "." + cleanFieldName
|
||||
r.resolver.registerJoin(jsonEach(jePair), jeAlias, nil)
|
||||
r.resolver.registerJoin(
|
||||
inflector.Columnify(newCollectionName),
|
||||
newTableAlias,
|
||||
dbx.NewExp(fmt.Sprintf("[[%s.id]] = [[%s.value]]", newTableAlias, jeAlias)),
|
||||
)
|
||||
r.activeCollectionName = newCollectionName
|
||||
r.activeTableAlias = newTableAlias
|
||||
// ---
|
||||
|
||||
// join the relation to the multi-match subquery
|
||||
// ---
|
||||
if options.MaxSelect == nil || *options.MaxSelect != 1 {
|
||||
if options.IsMultiple() {
|
||||
r.withMultiMatch = true
|
||||
}
|
||||
|
||||
newTableAlias2 := r.multiMatchActiveTableAlias + "_" + cleanFieldName
|
||||
jeAlias2 := r.multiMatchActiveTableAlias + "_" + cleanFieldName + "_je"
|
||||
jePair2 := r.multiMatchActiveTableAlias + "." + cleanFieldName
|
||||
r.multiMatchActiveTableAlias = newTableAlias2
|
||||
prefixedFieldName2 := r.multiMatchActiveTableAlias + "." + cleanFieldName
|
||||
|
||||
r.multiMatch.joins = append(
|
||||
r.multiMatch.joins,
|
||||
&join{
|
||||
tableName: jsonEach(jePair2),
|
||||
tableAlias: jeAlias2,
|
||||
},
|
||||
&join{
|
||||
tableName: inflector.Columnify(newCollectionName),
|
||||
tableAlias: newTableAlias2,
|
||||
on: dbx.NewExp(fmt.Sprintf("[[%s.id]] = [[%s.value]]", newTableAlias2, jeAlias2)),
|
||||
},
|
||||
)
|
||||
if !options.IsMultiple() {
|
||||
r.multiMatch.joins = append(
|
||||
r.multiMatch.joins,
|
||||
&join{
|
||||
tableName: inflector.Columnify(newCollectionName),
|
||||
tableAlias: newTableAlias2,
|
||||
on: dbx.NewExp(fmt.Sprintf("[[%s.id]] = [[%s]]", newTableAlias2, prefixedFieldName2)),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
jeAlias2 := r.multiMatchActiveTableAlias + "_" + cleanFieldName + "_je"
|
||||
r.multiMatch.joins = append(
|
||||
r.multiMatch.joins,
|
||||
&join{
|
||||
tableName: jsonEach(prefixedFieldName2),
|
||||
tableAlias: jeAlias2,
|
||||
},
|
||||
&join{
|
||||
tableName: inflector.Columnify(newCollectionName),
|
||||
tableAlias: newTableAlias2,
|
||||
on: dbx.NewExp(fmt.Sprintf("[[%s.id]] = [[%s.value]]", newTableAlias2, jeAlias2)),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
r.multiMatchActiveTableAlias = newTableAlias2
|
||||
// ---
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ func NewRecordFieldResolver(
|
||||
`^\@request\.auth\.[\w\.\:]*\w+$`,
|
||||
`^\@request\.data\.[\w\.\:]*\w+$`,
|
||||
`^\@request\.query\.[\w\.\:]*\w+$`,
|
||||
`^\@request\.headers\.\w+$`,
|
||||
`^\@collection\.\w+\.[\w\.\:]*\w+$`,
|
||||
},
|
||||
}
|
||||
@@ -92,6 +93,7 @@ func NewRecordFieldResolver(
|
||||
if r.requestData != nil {
|
||||
r.staticRequestData["method"] = r.requestData.Method
|
||||
r.staticRequestData["query"] = r.requestData.Query
|
||||
r.staticRequestData["headers"] = r.requestData.Headers
|
||||
r.staticRequestData["data"] = r.requestData.Data
|
||||
r.staticRequestData["auth"] = nil
|
||||
if r.requestData.AuthRecord != nil {
|
||||
@@ -132,6 +134,7 @@ func (r *RecordFieldResolver) UpdateQuery(query *dbx.SelectQuery) error {
|
||||
// project.screen.status
|
||||
// @request.status
|
||||
// @request.query.filter
|
||||
// @request.headers.x_token
|
||||
// @request.auth.someRelation.name
|
||||
// @request.data.someRelation.name
|
||||
// @request.data.someField
|
||||
@@ -249,10 +252,8 @@ func extractNestedMapVal(m map[string]any, keys ...string) (any, error) {
|
||||
return nil, fmt.Errorf("at least one key should be provided")
|
||||
}
|
||||
|
||||
var result any
|
||||
var ok bool
|
||||
|
||||
if result, ok = m[keys[0]]; !ok {
|
||||
result, ok := m[keys[0]]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid key path - missing key %q", keys[0])
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
}
|
||||
|
||||
requestData := &models.RequestData{
|
||||
Headers: map[string]any{
|
||||
"a": "123",
|
||||
"b": "456",
|
||||
},
|
||||
Query: map[string]any{
|
||||
"a": nil,
|
||||
"b": 123,
|
||||
@@ -55,14 +59,14 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"demo4",
|
||||
"title = true || title != 'test' || title ~ 'test1' || title !~ '%test2' || title > 1 || title >= 2 || title < 3 || title <= 4",
|
||||
false,
|
||||
"SELECT `demo4`.* FROM `demo4` WHERE (COALESCE([[demo4.title]], '') = COALESCE(1, '') OR COALESCE([[demo4.title]], '') != COALESCE({:TEST}, '') OR [[demo4.title]] LIKE {:TEST} ESCAPE '\\' OR [[demo4.title]] NOT LIKE {:TEST} ESCAPE '\\' OR [[demo4.title]] > {:TEST} OR [[demo4.title]] >= {:TEST} OR [[demo4.title]] < {:TEST} OR [[demo4.title]] <= {:TEST})",
|
||||
"SELECT `demo4`.* FROM `demo4` WHERE ([[demo4.title]] = 1 OR [[demo4.title]] != {:TEST} OR [[demo4.title]] LIKE {:TEST} ESCAPE '\\' OR [[demo4.title]] NOT LIKE {:TEST} ESCAPE '\\' OR [[demo4.title]] > {:TEST} OR [[demo4.title]] >= {:TEST} OR [[demo4.title]] < {:TEST} OR [[demo4.title]] <= {:TEST})",
|
||||
},
|
||||
{
|
||||
"non relation field (with all opt/any operators)",
|
||||
"demo4",
|
||||
"title ?= true || title ?!= 'test' || title ?~ 'test1' || title ?!~ '%test2' || title ?> 1 || title ?>= 2 || title ?< 3 || title ?<= 4",
|
||||
false,
|
||||
"SELECT `demo4`.* FROM `demo4` WHERE (COALESCE([[demo4.title]], '') = COALESCE(1, '') OR COALESCE([[demo4.title]], '') != COALESCE({:TEST}, '') OR [[demo4.title]] LIKE {:TEST} ESCAPE '\\' OR [[demo4.title]] NOT LIKE {:TEST} ESCAPE '\\' OR [[demo4.title]] > {:TEST} OR [[demo4.title]] >= {:TEST} OR [[demo4.title]] < {:TEST} OR [[demo4.title]] <= {:TEST})",
|
||||
"SELECT `demo4`.* FROM `demo4` WHERE ([[demo4.title]] = 1 OR [[demo4.title]] != {:TEST} OR [[demo4.title]] LIKE {:TEST} ESCAPE '\\' OR [[demo4.title]] NOT LIKE {:TEST} ESCAPE '\\' OR [[demo4.title]] > {:TEST} OR [[demo4.title]] >= {:TEST} OR [[demo4.title]] < {:TEST} OR [[demo4.title]] <= {:TEST})",
|
||||
},
|
||||
{
|
||||
"incomplete rel",
|
||||
@@ -76,21 +80,21 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"demo4",
|
||||
"self_rel_one.title > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_one]]) THEN [[demo4.self_rel_one]] ELSE json_array([[demo4.self_rel_one]]) END) `demo4_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4_self_rel_one_je.value]] WHERE [[demo4_self_rel_one.title]] > 1",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] WHERE [[demo4_self_rel_one.title]] > 1",
|
||||
},
|
||||
{
|
||||
"single rel (other collection)",
|
||||
"demo4",
|
||||
"rel_one_cascade.title > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.rel_one_cascade]]) THEN [[demo4.rel_one_cascade]] ELSE json_array([[demo4.rel_one_cascade]]) END) `demo4_rel_one_cascade_je` LEFT JOIN `demo3` `demo4_rel_one_cascade` ON [[demo4_rel_one_cascade.id]] = [[demo4_rel_one_cascade_je.value]] WHERE [[demo4_rel_one_cascade.title]] > 1",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo3` `demo4_rel_one_cascade` ON [[demo4_rel_one_cascade.id]] = [[demo4.rel_one_cascade]] WHERE [[demo4_rel_one_cascade.title]] > 1",
|
||||
},
|
||||
{
|
||||
"non-relation field + single rel",
|
||||
"demo4",
|
||||
"title > true || self_rel_one.title > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_one]]) THEN [[demo4.self_rel_one]] ELSE json_array([[demo4.self_rel_one]]) END) `demo4_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4_self_rel_one_je.value]] WHERE ([[demo4.title]] > 1 OR [[demo4_self_rel_one.title]] > 1)",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] WHERE ([[demo4.title]] > 1 OR [[demo4_self_rel_one.title]] > 1)",
|
||||
},
|
||||
{
|
||||
"nested incomplete rels (opt/any operator)",
|
||||
@@ -111,42 +115,42 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"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 json_each(CASE WHEN json_valid([[demo4_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_je` 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_je.value]] WHERE [[demo4_self_rel_many_self_rel_one.title]] > 1",
|
||||
"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",
|
||||
},
|
||||
{
|
||||
"nested complete rels (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 json_each(CASE WHEN json_valid([[demo4_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_je` 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_je.value]] 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 json_each(CASE WHEN json_valid([[__mm_demo4_self_rel_many.self_rel_one]]) THEN [[__mm_demo4_self_rel_many.self_rel_one]] ELSE json_array([[__mm_demo4_self_rel_many.self_rel_one]]) END) `__mm_demo4_self_rel_many_self_rel_one_je` 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_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{TEST}} WHERE ((NOT ([[TEST.multiMatchValue]] > 1)) OR ([[TEST.multiMatchValue]] IS NULL))))))",
|
||||
"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)) OR ([[__smTEST.multiMatchValue]] IS NULL))))))",
|
||||
},
|
||||
{
|
||||
"repeated nested rels (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 json_each(CASE WHEN json_valid([[demo4_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_je` 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_je.value]] 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 json_each(CASE WHEN json_valid([[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one_je` 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_je.value]] 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 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",
|
||||
},
|
||||
{
|
||||
"repeated nested rels (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 json_each(CASE WHEN json_valid([[demo4_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_je` 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_je.value]] 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 json_each(CASE WHEN json_valid([[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one_je` 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_je.value]] 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 json_each(CASE WHEN json_valid([[__mm_demo4_self_rel_many.self_rel_one]]) THEN [[__mm_demo4_self_rel_many.self_rel_one]] ELSE json_array([[__mm_demo4_self_rel_many.self_rel_one]]) END) `__mm_demo4_self_rel_many_self_rel_one_je` 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_je.value]] 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 json_each(CASE WHEN json_valid([[__mm_demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]]) THEN [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] ELSE json_array([[__mm_demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]]) END) `__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one_je` 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_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{TEST}} WHERE ((NOT ([[TEST.multiMatchValue]] > 1)) OR ([[TEST.multiMatchValue]] IS NULL))))))",
|
||||
"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)) OR ([[__smTEST.multiMatchValue]] IS NULL))))))",
|
||||
},
|
||||
{
|
||||
"multiple rels (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 json_each(CASE WHEN json_valid([[demo4.self_rel_one]]) THEN [[demo4.self_rel_one]] ELSE json_array([[demo4.self_rel_one]]) END) `demo4_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4_self_rel_one_je.value]] WHERE (COALESCE([[demo4_self_rel_many.title]], '') = COALESCE({:TEST}, '') OR JSON_EXTRACT([[demo4_self_rel_one.json_object]], '$.a') > 1)",
|
||||
"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 JSON_EXTRACT([[demo4_self_rel_one.json_object]], '$.a') > 1)",
|
||||
},
|
||||
{
|
||||
"multiple rels (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 json_each(CASE WHEN json_valid([[demo4.self_rel_one]]) THEN [[demo4.self_rel_one]] ELSE json_array([[demo4.self_rel_one]]) END) `demo4_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4_self_rel_one_je.value]] WHERE (((COALESCE([[demo4_self_rel_many.title]], '') = COALESCE({: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`) {{__TEST}} WHERE NOT (COALESCE([[__TEST.multiMatchValue]], '') = COALESCE({:TEST}, ''))))) OR JSON_EXTRACT([[demo4_self_rel_one.json_object]], '$.a') > 1)",
|
||||
"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 JSON_EXTRACT([[demo4_self_rel_one.json_object]], '$.a') > 1)",
|
||||
},
|
||||
{
|
||||
"@collection join (opt/any operators)",
|
||||
@@ -167,7 +171,7 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"demo4",
|
||||
"@request.auth.id > true || @request.auth.username > true || @request.auth.rel.title > true || @request.data.demo > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `users` `__auth_users` ON `__auth_users`.`id`={:TEST} LEFT JOIN json_each(CASE WHEN json_valid([[__auth_users.rel]]) THEN [[__auth_users.rel]] ELSE json_array([[__auth_users.rel]]) END) `__auth_users_rel_je` LEFT JOIN `demo2` `__auth_users_rel` ON [[__auth_users_rel.id]] = [[__auth_users_rel_je.value]] WHERE ({:TEST} > 1 OR {:TEST} > 1 OR [[__auth_users_rel.title]] > 1 OR NULL > 1)",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `users` `__auth_users` ON `__auth_users`.`id`={:TEST} LEFT JOIN `demo2` `__auth_users_rel` ON [[__auth_users_rel.id]] = [[__auth_users.rel]] WHERE ({:TEST} > 1 OR {:TEST} > 1 OR [[__auth_users_rel.title]] > 1 OR NULL > 1)",
|
||||
},
|
||||
{
|
||||
"hidden field with system filters (multi-match and ignore emailVisibility)",
|
||||
@@ -212,7 +216,7 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"@request.data.rel_many.url ?= 'test' &&" +
|
||||
"@request.data.rel_many.avatar ~ 'test'",
|
||||
false,
|
||||
"SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN `demo1` `__data_demo1` ON [[__data_demo1.id]]={:TEST} LEFT JOIN `users` `__data_users` ON [[__data_users.id]] IN ({:TEST}, {:TEST}) WHERE ({:TEST} > 1 AND [[__data_demo1.text]] > 1 AND {:TEST} > 1 AND ((COALESCE([[__data_users.email]], '') != COALESCE({:TEST}, '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__data_mm_users.email]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN `users` `__data_mm_users` ON `__data_mm_users`.`id` IN ({:TEST}, {:TEST}) WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE ((NOT (COALESCE([[__smTEST.multiMatchValue]], '') != COALESCE({:TEST}, ''))) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND COALESCE(NULL, '') = COALESCE({:TEST}, '') AND (([[__data_users.avatar]] LIKE {:TEST} ESCAPE '\\') AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__data_mm_users.avatar]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN `users` `__data_mm_users` ON `__data_mm_users`.`id` IN ({:TEST}, {:TEST}) WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] LIKE {:TEST} ESCAPE '\\')) OR ([[__smTEST.multiMatchValue]] IS NULL))))))",
|
||||
"SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN `demo1` `__data_demo1` ON [[__data_demo1.id]]={:TEST} LEFT JOIN `users` `__data_users` ON [[__data_users.id]] IN ({:TEST}, {:TEST}) WHERE ({:TEST} > 1 AND [[__data_demo1.text]] > 1 AND {:TEST} > 1 AND (([[__data_users.email]] != {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__data_mm_users.email]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN `users` `__data_mm_users` ON `__data_mm_users`.`id` IN ({:TEST}, {:TEST}) WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] != {:TEST})) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND '' = {:TEST} AND (([[__data_users.avatar]] LIKE {:TEST} ESCAPE '\\') AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__data_mm_users.avatar]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN `users` `__data_mm_users` ON `__data_mm_users`.`id` IN ({:TEST}, {:TEST}) WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] LIKE {:TEST} ESCAPE '\\')) OR ([[__smTEST.multiMatchValue]] IS NULL))))))",
|
||||
},
|
||||
{
|
||||
"@request.data.select:each fields",
|
||||
@@ -224,7 +228,7 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"@request.data.select_many:each = 'test' &&" +
|
||||
"@request.data.select_many:each ?< true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN json_each({:TEST}) `__dataSelect_select_one_je` LEFT JOIN json_each({:TEST}) `__dataSelect_select_many_je` WHERE (COALESCE(NULL, '') = COALESCE({:TEST}, '') AND COALESCE([[__dataSelect_select_one_je.value]], '') != COALESCE({:TEST}, '') AND COALESCE([[__dataSelect_select_one_je.value]], '') = COALESCE({:TEST}, '') AND {:TEST} LIKE {:TEST} ESCAPE '\\' AND ((COALESCE([[__dataSelect_select_many_je.value]], '') = COALESCE({:TEST}, '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm__dataSelect_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each({:TEST}) `__mm__dataSelect_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT (COALESCE([[__smTEST.multiMatchValue]], '') = COALESCE({:TEST}, ''))))) AND [[__dataSelect_select_many_je.value]] < 1)",
|
||||
"SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN json_each({:TEST}) `__dataSelect_select_one_je` LEFT JOIN json_each({:TEST}) `__dataSelect_select_many_je` WHERE ('' = {:TEST} AND [[__dataSelect_select_one_je.value]] != {:TEST} AND [[__dataSelect_select_one_je.value]] = {:TEST} AND {:TEST} LIKE {:TEST} ESCAPE '\\' AND (([[__dataSelect_select_many_je.value]] = {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm__dataSelect_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each({:TEST}) `__mm__dataSelect_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] = {:TEST})))) AND [[__dataSelect_select_many_je.value]] < 1)",
|
||||
},
|
||||
{
|
||||
"regular select:each fields",
|
||||
@@ -236,7 +240,7 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"select_many:each = 'test' &&" +
|
||||
"select_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` WHERE (COALESCE([[demo1.select_one]], '') = COALESCE({:TEST}, '') AND COALESCE([[demo1_select_one_je.value]], '') != COALESCE({:TEST}, '') AND [[demo1_select_one_je.value]] > 1 AND [[demo1.select_many]] LIKE {:TEST} ESCAPE '\\' AND ((COALESCE([[demo1_select_many_je.value]], '') = COALESCE({:TEST}, '')) 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([[__smTEST.multiMatchValue]], '') = COALESCE({:TEST}, ''))))) AND [[demo1_select_many_je.value]] > 1)",
|
||||
"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` WHERE ([[demo1.select_one]] = {:TEST} AND [[demo1_select_one_je.value]] != {:TEST} AND [[demo1_select_one_je.value]] > 1 AND [[demo1.select_many]] LIKE {:TEST} ESCAPE '\\' AND (([[demo1_select_many_je.value]] = {:TEST}) 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]] = {:TEST})))) AND [[demo1_select_many_je.value]] > 1)",
|
||||
},
|
||||
{
|
||||
"select:each vs select:each",
|
||||
@@ -246,7 +250,7 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"select_many:each ?< select_one:each &&" +
|
||||
"select_many:each = @request.data.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({:TEST}) `__dataSelect_select_many_je` WHERE (((COALESCE([[demo1_select_one_je.value]], '') != 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]], '') != COALESCE([[__smTEST.multiMatchValue]], ''))) OR ([[__smTEST.multiMatchValue]] IS NULL))))) 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]])) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND [[demo1_select_many_je.value]] < [[demo1_select_one_je.value]] AND ((COALESCE([[demo1_select_many_je.value]], '') = COALESCE([[__dataSelect_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__dataSelect_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each({:TEST}) `__mm__dataSelect_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 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({:TEST}) `__dataSelect_select_many_je` WHERE ((([[demo1_select_one_je.value]] != [[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 ([[demo1_select_one_je.value]] != [[__smTEST.multiMatchValue]])) OR ([[__smTEST.multiMatchValue]] IS NULL))))) 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]])) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND [[demo1_select_many_je.value]] < [[demo1_select_one_je.value]] AND (([[demo1_select_many_je.value]] = [[__dataSelect_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__dataSelect_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each({:TEST}) `__mm__dataSelect_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mrTEST}} WHERE NOT ([[__mlTEST.multiMatchValue]] = [[__mrTEST.multiMatchValue]])))))",
|
||||
},
|
||||
{
|
||||
"mixed multi-match vs multi-match",
|
||||
@@ -258,7 +262,7 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"@collection.demo2.active ?= rel_many.rel.active &&" +
|
||||
"rel_many.email > @request.data.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 json_each(CASE WHEN json_valid([[demo1_rel_many.rel]]) THEN [[demo1_rel_many.rel]] ELSE json_array([[demo1_rel_many.rel]]) END) `demo1_rel_many_rel_je` LEFT JOIN `demo2` `demo1_rel_many_rel` ON [[demo1_rel_many_rel.id]] = [[demo1_rel_many_rel_je.value]] 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 `demo1` `demo1_rel_one` ON [[demo1_rel_one.id]] = [[demo1_rel_one_je.value]] LEFT JOIN `demo2` `__collection_demo2` LEFT JOIN `users` `__data_users` ON [[__data_users.id]] IN ({:TEST}, {:TEST}) WHERE (((COALESCE([[demo1_rel_many_rel.active]], '') != 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 json_each(CASE WHEN json_valid([[__mm_demo1_rel_many.rel]]) THEN [[__mm_demo1_rel_many.rel]] ELSE json_array([[__mm_demo1_rel_many.rel]]) END) `__mm_demo1_rel_many_rel_je` LEFT JOIN `demo2` `__mm_demo1_rel_many_rel` ON [[__mm_demo1_rel_many_rel.id]] = [[__mm_demo1_rel_many_rel_je.value]] 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]], '') != COALESCE([[__mrTEST.multiMatchValue]], ''))) OR ([[__mlTEST.multiMatchValue]] IS NULL) OR ([[__mrTEST.multiMatchValue]] IS NULL))))) 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 json_each(CASE WHEN json_valid([[__mm_demo1_rel_many.rel]]) THEN [[__mm_demo1_rel_many.rel]] ELSE json_array([[__mm_demo1_rel_many.rel]]) END) `__mm_demo1_rel_many_rel_je` LEFT JOIN `demo2` `__mm_demo1_rel_many_rel` ON [[__mm_demo1_rel_many_rel.id]] = [[__mm_demo1_rel_many_rel_je.value]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] LIKE ('%' || [[demo1_rel_one.email]] || '%') ESCAPE '\\')) OR ([[__smTEST.multiMatchValue]] IS NULL))))) 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 json_each(CASE WHEN json_valid([[__mm_demo1_rel_many.rel]]) THEN [[__mm_demo1_rel_many.rel]] ELSE json_array([[__mm_demo1_rel_many.rel]]) END) `__mm_demo1_rel_many_rel_je` LEFT JOIN `demo2` `__mm_demo1_rel_many_rel` ON [[__mm_demo1_rel_many_rel.id]] = [[__mm_demo1_rel_many_rel_je.value]] 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.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` LEFTLine truncated
|
||||
"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` ON [[__data_users.id]] IN ({:TEST}, {:TEST}) WHERE ((([[demo1_rel_many_rel.active]] != [[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 ([[__mlTEST.multiMatchValue]] != [[__mrTEST.multiMatchValue]])) OR ([[__mlTEST.multiMatchValue]] IS NULL) OR ([[__mrTEST.multiMatchValue]] IS NULL))))) AND [[demo1_rel_many_rel.active]] = [[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 '\\')) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND (([[__collection_demo2.active]] = [[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 ([[__mlTEST.multiMatchValue]] = [[__mrTEST.multiMatchValue]])))) AND [[__collection_demo2.active]] = [[demo1_rel_many_rel.active]] AND (((([[demo1_rel_many.email]] > [[__data_users.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.email]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN `users` `__data_mm_users` ON `__data_mm_users`.`id` IN ({:TEST}, {:TEST}) WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mrTEST}} WHERE ((NOT ([[__mlTEST.multiMatchValue]] > [[__mrTEST.multiMatchValue]])) OR ([[__mlTEST.multiMatchValue]] IS NULL) OR ([[__mrTEST.multiMatchValue]] IS NULL)))))) AND ([[demo1_rel_many.emailVisibility]] = TRUE)))",
|
||||
},
|
||||
{
|
||||
"@request.data.arrayable:length fields",
|
||||
@@ -276,7 +280,7 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
"@request.data.file_many:length != 1 &&" +
|
||||
"@request.data.file_many:length ?!= 2",
|
||||
false,
|
||||
"SELECT `demo1`.* FROM `demo1` WHERE (0 > {:TEST} AND 0 > {:TEST} AND 2 < {:TEST} AND 2 > {:TEST} AND COALESCE(1, '') = COALESCE({:TEST}, '') AND COALESCE(1, '') = COALESCE({:TEST}, '') AND COALESCE(2, '') != COALESCE({:TEST}, '') AND COALESCE(2, '') != COALESCE({:TEST}, '') AND COALESCE(1, '') = COALESCE({:TEST}, '') AND COALESCE(1, '') = COALESCE({:TEST}, '') AND COALESCE(3, '') != COALESCE({:TEST}, '') AND COALESCE(3, '') != COALESCE({:TEST}, ''))",
|
||||
"SELECT `demo1`.* FROM `demo1` WHERE (0 > {:TEST} AND 0 > {:TEST} AND 2 < {:TEST} AND 2 > {:TEST} AND 1 = {:TEST} AND 1 = {:TEST} AND 2 != {:TEST} AND 2 != {:TEST} AND 1 = {:TEST} AND 1 = {:TEST} AND 3 != {:TEST} AND 3 != {:TEST})",
|
||||
},
|
||||
{
|
||||
"regular arrayable:length fields",
|
||||
@@ -290,7 +294,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` ON [[__data_demo4.id]]={:TEST} LEFT JOIN `demo3` `__data_demo3` ON [[__data_demo3.id]] IN ({:TEST}, {:TEST}) LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_one]]) THEN [[demo4.self_rel_one]] ELSE json_array([[demo4.self_rel_one]]) END) `demo4_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4_self_rel_one_je.value]] 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_many]]) THEN [[__data_demo4.self_rel_many]] ELSE json_array([[__data_demo4.self_rel_many]]) END) > {:TEST} AND json_array_length(CASE WHEN json_valid([[__data_demo4.self_rel_many]]) THEN [[__data_demo4.self_rel_many]] ELSE json_array([[__data_demo4.self_rel_many]]) END) > {:TEST} AND json_array_length(CASE WHEN json_valid([[__data_demo3.files]]) THEN [[__data_demo3.files]] ELSE json_array([[__data_demo3.files]]) END) < {:TEST} AND ((json_array_length(CASE WHEN json_valid([[__data_demo3.files]]) THEN [[__data_demo3.files]] ELSE json_array([[__data_demo3.files]]) END) < {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT json_array_length(CASE WHEN json_valid([[__data_mm_demo3.files]]) THEN [[__data_mm_demo3.files]] ELSE json_array([[__data_mm_demo3.files]]) END) as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN `demo3` `__data_mm_demo3` ON `__data_mm_demo3`.`id` IN ({:TEST}, {:TEST}) WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] < {:TEST})) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND COALESCE(json_array_length(CASE WHEN json_valid([[demo4_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_one.self_rel_many]]) END), '') = COALESCE({:TEST}, '') AND COALESCE(json_array_length(CASE WHEN json_valid([[demo4_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_one.self_rel_many]]) END), '') = COALESCE({:TEST}, '') AND ((COALESCE(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 json_array([[demo4_self_rel_one_rel_many_cascade.files]]) END), '') != COALESCE({: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 json_array([[__mm_demo4_self_rel_one_rel_many_cascade.files]]) END) as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_one]]) THEN [[__mm_demo4.self_rel_one]] ELSE json_array([[__mm_demo4.self_rel_one]]) END) `__mm_demo4_self_rel_one_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_one` ON [[__mm_demo4_self_rel_one.id]] = [[__mm_demo4_self_rel_one_je.value]] 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_many_cascade_je` LEFT JOIN `demo3` `__mm_demo4_self_rel_one_rel_many_cascade` ON [[__mm_demo4_self_rel_one_rel_many_cascade.id]] = [[__mm_demo4_self_rel_one_rel_many_cascade_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE ((NOT (COALESCE([[__smTEST.multiMatchValue]], '') != COALESCE({:TEST}, ''))) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND COALESCE(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 json_array([[demo4_self_rel_one_rel_many_cascade.files]]) END), '') != COALESCE({:TEST}, ''))",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `__data_demo4` ON [[__data_demo4.id]]={:TEST} LEFT JOIN `demo3` `__data_demo3` ON [[__data_demo3.id]] IN ({:TEST}, {:TEST}) 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_many]]) THEN [[__data_demo4.self_rel_many]] ELSE json_array([[__data_demo4.self_rel_many]]) END) > {:TEST} AND json_array_length(CASE WHEN json_valid([[__data_demo4.self_rel_many]]) THEN [[__data_demo4.self_rel_many]] ELSE json_array([[__data_demo4.self_rel_many]]) END) > {:TEST} AND json_array_length(CASE WHEN json_valid([[__data_demo3.files]]) THEN [[__data_demo3.files]] ELSE json_array([[__data_demo3.files]]) END) < {:TEST} AND ((json_array_length(CASE WHEN json_valid([[__data_demo3.files]]) THEN [[__data_demo3.files]] ELSE json_array([[__data_demo3.files]]) END) < {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT json_array_length(CASE WHEN json_valid([[__data_mm_demo3.files]]) THEN [[__data_mm_demo3.files]] ELSE json_array([[__data_mm_demo3.files]]) END) as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN `demo3` `__data_mm_demo3` ON `__data_mm_demo3`.`id` IN ({:TEST}, {:TEST}) WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] < {:TEST})) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND COALESCE(json_array_length(CASE WHEN json_valid([[demo4_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_one.self_rel_many]]) END), '') = {:TEST} AND COALESCE(json_array_length(CASE WHEN json_valid([[demo4_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_one.self_rel_many]]) END), '') = {:TEST} AND ((COALESCE(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 json_array([[demo4_self_rel_one_rel_many_cascade.files]]) END), '') != {: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 json_array([[__mm_demo4_self_rel_one_rel_many_cascade.files]]) 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_many_cascade_je` LEFT JOIN `demo3` `__mm_demo4_self_rel_one_rel_many_cascade` ON [[__mm_demo4_self_rel_one_rel_many_cascade.id]] = [[__mm_demo4_self_rel_one_rel_many_cascade_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] != {:TEST})) OR ([[__smTEST.multiMatchValue]] IS NULL))))) AND COALESCE(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 json_array([[demo4_self_rel_one_rel_many_cascade.files]]) END), '') != {:TEST})",
|
||||
},
|
||||
{
|
||||
"json_extract and json_array_length COALESCE equal normalizations",
|
||||
"demo4",
|
||||
"json_object.a.b = 1 && self_rel_many:length != 2 && json_object.a.b > 3 && self_rel_many:length <= 4",
|
||||
false,
|
||||
"SELECT `demo4`.* FROM `demo4` WHERE (COALESCE(JSON_EXTRACT([[demo4.json_object]], '$.a.b'), '') = {:TEST} AND COALESCE(json_array_length(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END), '') != {:TEST} AND JSON_EXTRACT([[demo4.json_object]], '$.a.b') > {:TEST} AND json_array_length(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) <= {:TEST})",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -438,6 +449,9 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
|
||||
"b": 456,
|
||||
"c": map[string]int{"sub": 1},
|
||||
},
|
||||
Headers: map[string]any{
|
||||
"d": "789",
|
||||
},
|
||||
AuthRecord: authRecord,
|
||||
}
|
||||
|
||||
@@ -456,6 +470,10 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
|
||||
{"@request.query", true, ``},
|
||||
{"@request.query.a", false, `123`},
|
||||
{"@request.query.a.missing", false, ``},
|
||||
{"@request.headers", true, ``},
|
||||
{"@request.headers.missing", false, ``},
|
||||
{"@request.headers.d", false, `"789"`},
|
||||
{"@request.headers.d.sub", true, ``},
|
||||
{"@request.data", true, ``},
|
||||
{"@request.data.b", false, `456`},
|
||||
{"@request.data.number", false, `10`}, // number field normalization
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,222 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/spf13/cast"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
var _ Provider = (*Apple)(nil)
|
||||
|
||||
// NameApple is the unique name of the Apple provider.
|
||||
const NameApple string = "apple"
|
||||
|
||||
// Apple allows authentication via Apple OAuth2.
|
||||
//
|
||||
// [OIDC differences]: https://bitbucket.org/openid/connect/src/master/How-Sign-in-with-Apple-differs-from-OpenID-Connect.md
|
||||
type Apple struct {
|
||||
*baseProvider
|
||||
|
||||
jwksUrl string
|
||||
}
|
||||
|
||||
// NewAppleProvider creates a new Apple provider instance with some defaults.
|
||||
func NewAppleProvider() *Apple {
|
||||
return &Apple{
|
||||
baseProvider: &baseProvider{
|
||||
scopes: nil, // custom scopes are currently not supported since they require a POST redirect
|
||||
ctx: context.Background(),
|
||||
authUrl: "https://appleid.apple.com/auth/authorize",
|
||||
tokenUrl: "https://appleid.apple.com/auth/token",
|
||||
},
|
||||
jwksUrl: "https://appleid.apple.com/auth/keys",
|
||||
}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the provided token.
|
||||
//
|
||||
// API reference: https://developer.apple.com/documentation/sign_in_with_apple/tokenresponse.
|
||||
func (p *Apple) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserData(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:"sub"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified any `json:"email_verified"` // could be string or bool
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
if cast.ToBool(extracted.EmailVerified) {
|
||||
user.Email = extracted.Email
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// FetchRawUserData implements Provider.FetchRawUserData interface.
|
||||
//
|
||||
// Apple doesn't have a UserInfo endpoint and claims about users
|
||||
// are instead included in the "id_token" (https://openid.net/specs/openid-connect-core-1_0.html#id_tokenExample)
|
||||
func (p *Apple) FetchRawUserData(token *oauth2.Token) ([]byte, error) {
|
||||
idToken, _ := token.Extra("id_token").(string)
|
||||
|
||||
claims, err := p.parseAndVerifyIdToken(idToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(claims)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
func (p *Apple) parseAndVerifyIdToken(idToken string) (jwt.MapClaims, error) {
|
||||
if idToken == "" {
|
||||
return nil, errors.New("empty id_token")
|
||||
}
|
||||
|
||||
// extract the token header params and claims
|
||||
// ---
|
||||
claims := jwt.MapClaims{}
|
||||
t, _, err := jwt.NewParser().ParseUnverified(idToken, claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// validate common claims per https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/verifying_a_user#3383769
|
||||
// ---
|
||||
if !claims.VerifyIssuer("https://appleid.apple.com", true) {
|
||||
return nil, errors.New("iss must be https://appleid.apple.com")
|
||||
}
|
||||
|
||||
if !claims.VerifyAudience(p.clientId, true) {
|
||||
return nil, errors.New("aud must be the developer's client_id")
|
||||
}
|
||||
|
||||
// fetch the public key set
|
||||
// ---
|
||||
kid, _ := t.Header["kid"].(string)
|
||||
if kid == "" {
|
||||
return nil, errors.New("missing kid header value")
|
||||
}
|
||||
|
||||
key, err := p.fetchJWK(kid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// decode the key params per RFC 7518 (https://tools.ietf.org/html/rfc7518#section-6.3)
|
||||
// and construct a valid publicKey from them
|
||||
// ---
|
||||
exponent, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(key.E, "="))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
modulus, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(key.N, "="))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publicKey := &rsa.PublicKey{
|
||||
// https://tools.ietf.org/html/rfc7517#appendix-A.1
|
||||
E: int(big.NewInt(0).SetBytes(exponent).Uint64()),
|
||||
N: big.NewInt(0).SetBytes(modulus),
|
||||
}
|
||||
|
||||
// verify the id_token
|
||||
// ---
|
||||
parser := jwt.NewParser(jwt.WithValidMethods([]string{key.Alg}))
|
||||
|
||||
parsedToken, err := parser.Parse(idToken, func(t *jwt.Token) (any, error) {
|
||||
return publicKey, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := parsedToken.Claims.(jwt.MapClaims); ok && parsedToken.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("the parsed id_token is invalid")
|
||||
}
|
||||
|
||||
type jwk struct {
|
||||
Kty string
|
||||
Kid string
|
||||
Use string
|
||||
Alg string
|
||||
N string
|
||||
E string
|
||||
}
|
||||
|
||||
func (p *Apple) fetchJWK(kid string) (*jwk, error) {
|
||||
req, err := http.NewRequestWithContext(p.ctx, "GET", p.jwksUrl, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
rawBody, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// http.Client.Get doesn't treat non 2xx responses as error
|
||||
if res.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf(
|
||||
"failed to verify the provided id_token (%d):\n%s",
|
||||
res.StatusCode,
|
||||
string(rawBody),
|
||||
)
|
||||
}
|
||||
|
||||
jwks := struct {
|
||||
Keys []*jwk
|
||||
}{}
|
||||
if err := json.Unmarshal(rawBody, &jwks); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, key := range jwks.Keys {
|
||||
if key.Kid == kid {
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("jwk with kid %q was not found", kid)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
@@ -21,6 +22,12 @@ type AuthUser struct {
|
||||
|
||||
// Provider defines a common interface for an OAuth2 client.
|
||||
type Provider interface {
|
||||
// Scopes returns the context associated with the provider (if any).
|
||||
Context() context.Context
|
||||
|
||||
// SetContext assigns the specified context to the current provider.
|
||||
SetContext(ctx context.Context)
|
||||
|
||||
// Scopes returns the provider access permissions that will be requested.
|
||||
Scopes() []string
|
||||
|
||||
@@ -120,6 +127,8 @@ func NewProviderByName(name string) (Provider, error) {
|
||||
return NewOIDCProvider(), nil
|
||||
case NameOIDC + "3":
|
||||
return NewOIDCProvider(), nil
|
||||
case NameApple:
|
||||
return NewAppleProvider(), nil
|
||||
default:
|
||||
return nil, errors.New("Missing provider " + name)
|
||||
}
|
||||
|
||||
@@ -171,4 +171,13 @@ func TestNewProviderByName(t *testing.T) {
|
||||
if _, ok := p.(*auth.OIDC); !ok {
|
||||
t.Error("Expected to be instance of *auth.OIDC")
|
||||
}
|
||||
|
||||
// apple
|
||||
p, err = auth.NewProviderByName(auth.NameApple)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Apple); !ok {
|
||||
t.Error("Expected to be instance of *auth.Apple")
|
||||
}
|
||||
}
|
||||
+38
-27
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
// baseProvider defines common fields and methods used by OAuth2 client providers.
|
||||
type baseProvider struct {
|
||||
ctx context.Context
|
||||
scopes []string
|
||||
clientId string
|
||||
clientSecret string
|
||||
@@ -20,94 +21,104 @@ type baseProvider struct {
|
||||
userApiUrl string
|
||||
}
|
||||
|
||||
// Scopes implements Provider.Scopes interface.
|
||||
// Context implements Provider.Context() interface method.
|
||||
func (p *baseProvider) Context() context.Context {
|
||||
return p.ctx
|
||||
}
|
||||
|
||||
// SetContext implements Provider.SetContext() interface method.
|
||||
func (p *baseProvider) SetContext(ctx context.Context) {
|
||||
p.ctx = ctx
|
||||
}
|
||||
|
||||
// Scopes implements Provider.Scopes() interface method.
|
||||
func (p *baseProvider) Scopes() []string {
|
||||
return p.scopes
|
||||
}
|
||||
|
||||
// SetScopes implements Provider.SetScopes interface.
|
||||
// SetScopes implements Provider.SetScopes() interface method.
|
||||
func (p *baseProvider) SetScopes(scopes []string) {
|
||||
p.scopes = scopes
|
||||
}
|
||||
|
||||
// ClientId implements Provider.ClientId interface.
|
||||
// ClientId implements Provider.ClientId() interface method.
|
||||
func (p *baseProvider) ClientId() string {
|
||||
return p.clientId
|
||||
}
|
||||
|
||||
// SetClientId implements Provider.SetClientId interface.
|
||||
// SetClientId implements Provider.SetClientId() interface method.
|
||||
func (p *baseProvider) SetClientId(clientId string) {
|
||||
p.clientId = clientId
|
||||
}
|
||||
|
||||
// ClientSecret implements Provider.ClientSecret interface.
|
||||
// ClientSecret implements Provider.ClientSecret() interface method.
|
||||
func (p *baseProvider) ClientSecret() string {
|
||||
return p.clientSecret
|
||||
}
|
||||
|
||||
// SetClientSecret implements Provider.SetClientSecret interface.
|
||||
// SetClientSecret implements Provider.SetClientSecret() interface method.
|
||||
func (p *baseProvider) SetClientSecret(secret string) {
|
||||
p.clientSecret = secret
|
||||
}
|
||||
|
||||
// RedirectUrl implements Provider.RedirectUrl interface.
|
||||
// RedirectUrl implements Provider.RedirectUrl() interface method.
|
||||
func (p *baseProvider) RedirectUrl() string {
|
||||
return p.redirectUrl
|
||||
}
|
||||
|
||||
// SetRedirectUrl implements Provider.SetRedirectUrl interface.
|
||||
// SetRedirectUrl implements Provider.SetRedirectUrl() interface method.
|
||||
func (p *baseProvider) SetRedirectUrl(url string) {
|
||||
p.redirectUrl = url
|
||||
}
|
||||
|
||||
// AuthUrl implements Provider.AuthUrl interface.
|
||||
// AuthUrl implements Provider.AuthUrl() interface method.
|
||||
func (p *baseProvider) AuthUrl() string {
|
||||
return p.authUrl
|
||||
}
|
||||
|
||||
// SetAuthUrl implements Provider.SetAuthUrl interface.
|
||||
// SetAuthUrl implements Provider.SetAuthUrl() interface method.
|
||||
func (p *baseProvider) SetAuthUrl(url string) {
|
||||
p.authUrl = url
|
||||
}
|
||||
|
||||
// TokenUrl implements Provider.TokenUrl interface.
|
||||
// TokenUrl implements Provider.TokenUrl() interface method.
|
||||
func (p *baseProvider) TokenUrl() string {
|
||||
return p.tokenUrl
|
||||
}
|
||||
|
||||
// SetTokenUrl implements Provider.SetTokenUrl interface.
|
||||
// SetTokenUrl implements Provider.SetTokenUrl() interface method.
|
||||
func (p *baseProvider) SetTokenUrl(url string) {
|
||||
p.tokenUrl = url
|
||||
}
|
||||
|
||||
// UserApiUrl implements Provider.UserApiUrl interface.
|
||||
// UserApiUrl implements Provider.UserApiUrl() interface method.
|
||||
func (p *baseProvider) UserApiUrl() string {
|
||||
return p.userApiUrl
|
||||
}
|
||||
|
||||
// SetUserApiUrl implements Provider.SetUserApiUrl interface.
|
||||
// SetUserApiUrl implements Provider.SetUserApiUrl() interface method.
|
||||
func (p *baseProvider) SetUserApiUrl(url string) {
|
||||
p.userApiUrl = url
|
||||
}
|
||||
|
||||
// BuildAuthUrl implements Provider.BuildAuthUrl interface.
|
||||
// BuildAuthUrl implements Provider.BuildAuthUrl() interface method.
|
||||
func (p *baseProvider) BuildAuthUrl(state string, opts ...oauth2.AuthCodeOption) string {
|
||||
return p.oauth2Config().AuthCodeURL(state, opts...)
|
||||
}
|
||||
|
||||
// FetchToken implements Provider.FetchToken interface.
|
||||
// FetchToken implements Provider.FetchToken() interface method.
|
||||
func (p *baseProvider) FetchToken(code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||
return p.oauth2Config().Exchange(context.Background(), code, opts...)
|
||||
return p.oauth2Config().Exchange(p.ctx, code, opts...)
|
||||
}
|
||||
|
||||
// Client implements Provider.Client interface.
|
||||
// Client implements Provider.Client() interface method.
|
||||
func (p *baseProvider) Client(token *oauth2.Token) *http.Client {
|
||||
return p.oauth2Config().Client(context.Background(), token)
|
||||
return p.oauth2Config().Client(p.ctx, token)
|
||||
}
|
||||
|
||||
// FetchRawUserData implements Provider.FetchRawUserData interface.
|
||||
// FetchRawUserData implements Provider.FetchRawUserData() interface method.
|
||||
func (p *baseProvider) FetchRawUserData(token *oauth2.Token) ([]byte, error) {
|
||||
req, err := http.NewRequest("GET", p.userApiUrl, nil)
|
||||
req, err := http.NewRequestWithContext(p.ctx, "GET", p.userApiUrl, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -119,23 +130,23 @@ func (p *baseProvider) FetchRawUserData(token *oauth2.Token) ([]byte, error) {
|
||||
func (p *baseProvider) sendRawUserDataRequest(req *http.Request, token *oauth2.Token) ([]byte, error) {
|
||||
client := p.Client(token)
|
||||
|
||||
response, err := client.Do(req)
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
defer res.Body.Close()
|
||||
|
||||
result, err := io.ReadAll(response.Body)
|
||||
result, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// http.Client.Get doesn't treat non 2xx responses as error
|
||||
if response.StatusCode >= 400 {
|
||||
if res.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf(
|
||||
"Failed to fetch OAuth2 user profile via %s (%d):\n%s",
|
||||
"failed to fetch OAuth2 user profile via %s (%d):\n%s",
|
||||
p.userApiUrl,
|
||||
response.StatusCode,
|
||||
res.StatusCode,
|
||||
string(result),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func TestContext(t *testing.T) {
|
||||
b := baseProvider{}
|
||||
|
||||
before := b.Scopes()
|
||||
if before != nil {
|
||||
t.Errorf("Expected nil context, got %v", before)
|
||||
}
|
||||
|
||||
b.SetContext(context.Background())
|
||||
|
||||
after := b.Scopes()
|
||||
if after != nil {
|
||||
t.Error("Expected non-nil context")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopes(t *testing.T) {
|
||||
b := baseProvider{}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
@@ -22,6 +23,7 @@ func NewDiscordProvider() *Discord {
|
||||
// https://discord.com/developers/docs/topics/oauth2
|
||||
// https://discord.com/developers/docs/resources/user#get-current-user
|
||||
return &Discord{&baseProvider{
|
||||
ctx: context.Background(),
|
||||
scopes: []string{"identify", "email"},
|
||||
authUrl: "https://discord.com/api/oauth2/authorize",
|
||||
tokenUrl: "https://discord.com/api/oauth2/token",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
@@ -20,6 +21,7 @@ type Facebook struct {
|
||||
// NewFacebookProvider creates new Facebook provider instance with some defaults.
|
||||
func NewFacebookProvider() *Facebook {
|
||||
return &Facebook{&baseProvider{
|
||||
ctx: context.Background(),
|
||||
scopes: []string{"email"},
|
||||
authUrl: facebook.Endpoint.AuthURL,
|
||||
tokenUrl: facebook.Endpoint.TokenURL,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
@@ -20,6 +21,7 @@ type Gitea struct {
|
||||
// NewGiteaProvider creates new Gitea provider instance with some defaults.
|
||||
func NewGiteaProvider() *Gitea {
|
||||
return &Gitea{&baseProvider{
|
||||
ctx: context.Background(),
|
||||
scopes: []string{"read:user", "user:email"},
|
||||
authUrl: "https://gitea.com/login/oauth/authorize",
|
||||
tokenUrl: "https://gitea.com/login/oauth/access_token",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strconv"
|
||||
@@ -22,6 +23,7 @@ type Gitee struct {
|
||||
// NewGiteeProvider creates new Gitee provider instance with some defaults.
|
||||
func NewGiteeProvider() *Gitee {
|
||||
return &Gitee{&baseProvider{
|
||||
ctx: context.Background(),
|
||||
scopes: []string{"user_info", "emails"},
|
||||
authUrl: "https://gitee.com/oauth/authorize",
|
||||
tokenUrl: "https://gitee.com/oauth/token",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strconv"
|
||||
@@ -22,6 +23,7 @@ type Github struct {
|
||||
// NewGithubProvider creates new Github provider instance with some defaults.
|
||||
func NewGithubProvider() *Github {
|
||||
return &Github{&baseProvider{
|
||||
ctx: context.Background(),
|
||||
scopes: []string{"read:user", "user:email"},
|
||||
authUrl: github.Endpoint.AuthURL,
|
||||
tokenUrl: github.Endpoint.TokenURL,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
@@ -20,6 +21,7 @@ type Gitlab struct {
|
||||
// NewGitlabProvider creates new Gitlab provider instance with some defaults.
|
||||
func NewGitlabProvider() *Gitlab {
|
||||
return &Gitlab{&baseProvider{
|
||||
ctx: context.Background(),
|
||||
scopes: []string{"read_user"},
|
||||
authUrl: "https://gitlab.com/oauth/authorize",
|
||||
tokenUrl: "https://gitlab.com/oauth/token",
|
||||
|
||||
+11
-5
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
@@ -19,6 +20,7 @@ type Google struct {
|
||||
// NewGoogleProvider creates new Google provider instance with some defaults.
|
||||
func NewGoogleProvider() *Google {
|
||||
return &Google{&baseProvider{
|
||||
ctx: context.Background(),
|
||||
scopes: []string{
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
@@ -42,10 +44,11 @@ func (p *Google) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Id string
|
||||
Name string
|
||||
Email string
|
||||
Picture string
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Picture string `json:"picture"`
|
||||
VerifiedEmail bool `json:"verified_email"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
@@ -54,12 +57,15 @@ func (p *Google) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
Email: extracted.Email,
|
||||
AvatarUrl: extracted.Picture,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
if extracted.VerifiedEmail {
|
||||
user.Email = extracted.Email
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
@@ -21,6 +22,7 @@ type Kakao struct {
|
||||
// NewKakaoProvider creates a new Kakao provider instance with some defaults.
|
||||
func NewKakaoProvider() *Kakao {
|
||||
return &Kakao{&baseProvider{
|
||||
ctx: context.Background(),
|
||||
scopes: []string{"account_email", "profile_nickname", "profile_image"},
|
||||
authUrl: kakao.Endpoint.AuthURL,
|
||||
tokenUrl: kakao.Endpoint.TokenURL,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
@@ -19,6 +20,7 @@ type Livechat struct {
|
||||
// NewLivechatProvider creates new Livechat provider instance with some defaults.
|
||||
func NewLivechatProvider() *Livechat {
|
||||
return &Livechat{&baseProvider{
|
||||
ctx: context.Background(),
|
||||
scopes: []string{}, // default scopes are specified from the provider dashboard
|
||||
authUrl: "https://accounts.livechat.com/",
|
||||
tokenUrl: "https://accounts.livechat.com/token",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
@@ -21,6 +22,7 @@ type Microsoft struct {
|
||||
func NewMicrosoftProvider() *Microsoft {
|
||||
endpoints := microsoft.AzureADEndpoint("")
|
||||
return &Microsoft{&baseProvider{
|
||||
ctx: context.Background(),
|
||||
scopes: []string{"User.Read"},
|
||||
authUrl: endpoints.AuthURL,
|
||||
tokenUrl: endpoints.TokenURL,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
@@ -19,6 +20,7 @@ type OIDC struct {
|
||||
// NewOIDCProvider creates new OpenID Connect (OIDC) provider instance with some defaults.
|
||||
func NewOIDCProvider() *OIDC {
|
||||
return &OIDC{&baseProvider{
|
||||
ctx: context.Background(),
|
||||
scopes: []string{
|
||||
"openid", // minimal requirement to return the id
|
||||
"email",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
@@ -20,6 +21,7 @@ type Spotify struct {
|
||||
// NewSpotifyProvider creates a new Spotify provider instance with some defaults.
|
||||
func NewSpotifyProvider() *Spotify {
|
||||
return &Spotify{&baseProvider{
|
||||
ctx: context.Background(),
|
||||
scopes: []string{
|
||||
"user-read-private",
|
||||
// currently Spotify doesn't return information whether the email is verified or not
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
@@ -20,6 +21,7 @@ type Strava struct {
|
||||
// NewStravaProvider creates new Strava provider instance with some defaults.
|
||||
func NewStravaProvider() *Strava {
|
||||
return &Strava{&baseProvider{
|
||||
ctx: context.Background(),
|
||||
scopes: []string{
|
||||
"profile:read_all",
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
@@ -22,6 +23,7 @@ type Twitch struct {
|
||||
// NewTwitchProvider creates new Twitch provider instance with some defaults.
|
||||
func NewTwitchProvider() *Twitch {
|
||||
return &Twitch{&baseProvider{
|
||||
ctx: context.Background(),
|
||||
scopes: []string{"user:read:email"},
|
||||
authUrl: twitch.Endpoint.AuthURL,
|
||||
tokenUrl: twitch.Endpoint.TokenURL,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
@@ -19,6 +20,7 @@ type Twitter struct {
|
||||
// NewTwitterProvider creates new Twitter provider instance with some defaults.
|
||||
func NewTwitterProvider() *Twitter {
|
||||
return &Twitter{&baseProvider{
|
||||
ctx: context.Background(),
|
||||
scopes: []string{
|
||||
"users.read",
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package dbutils
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/tokenizer"
|
||||
)
|
||||
|
||||
var (
|
||||
indexRegex = regexp.MustCompile(`(?im)create\s+(unique\s+)?\s*index\s*(if\s+not\s+exists\s+)?(\S*)\s+on\s+(\S*)\s+\(([\s\S]*)\)(?:\s*where\s+([\s\S]*))?`)
|
||||
indexColumnRegex = regexp.MustCompile(`(?im)^([\s\S]+?)(?:\s+collate\s+([\w]+))?(?:\s+(asc|desc))?$`)
|
||||
)
|
||||
|
||||
// IndexColumn represents a single parsed SQL index column.
|
||||
type IndexColumn struct {
|
||||
Name string `json:"name"` // identifier or expression
|
||||
Collate string `json:"collate"`
|
||||
Sort string `json:"sort"`
|
||||
}
|
||||
|
||||
// Index represents a single parsed SQL CREATE INDEX expression.
|
||||
type Index struct {
|
||||
Unique bool `json:"unique"`
|
||||
Optional bool `json:"optional"`
|
||||
SchemaName string `json:"schemaName"`
|
||||
IndexName string `json:"indexName"`
|
||||
TableName string `json:"tableName"`
|
||||
Columns []IndexColumn `json:"columns"`
|
||||
Where string `json:"where"`
|
||||
}
|
||||
|
||||
// IsValid checks if the current Index contains the minimum required fields to be considered valid.
|
||||
func (idx Index) IsValid() bool {
|
||||
return idx.IndexName != "" && idx.TableName != "" && len(idx.Columns) > 0
|
||||
}
|
||||
|
||||
// Build returns a "CREATE INDEX" SQL string from the current index parts.
|
||||
//
|
||||
// Returns empty string if idx.IsValid() is false.
|
||||
func (idx Index) Build() string {
|
||||
if !idx.IsValid() {
|
||||
return ""
|
||||
}
|
||||
|
||||
var str strings.Builder
|
||||
|
||||
str.WriteString("CREATE ")
|
||||
|
||||
if idx.Unique {
|
||||
str.WriteString("UNIQUE ")
|
||||
}
|
||||
|
||||
str.WriteString("INDEX ")
|
||||
|
||||
if idx.Optional {
|
||||
str.WriteString("IF NOT EXISTS ")
|
||||
}
|
||||
|
||||
if idx.SchemaName != "" {
|
||||
str.WriteString("`")
|
||||
str.WriteString(idx.SchemaName)
|
||||
str.WriteString("`.")
|
||||
}
|
||||
|
||||
str.WriteString("`")
|
||||
str.WriteString(idx.IndexName)
|
||||
str.WriteString("` ")
|
||||
|
||||
str.WriteString("ON `")
|
||||
str.WriteString(idx.TableName)
|
||||
str.WriteString("` (")
|
||||
|
||||
if len(idx.Columns) > 1 {
|
||||
str.WriteString("\n ")
|
||||
}
|
||||
|
||||
var hasCol bool
|
||||
for _, col := range idx.Columns {
|
||||
trimmedColName := strings.TrimSpace(col.Name)
|
||||
if trimmedColName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if hasCol {
|
||||
str.WriteString(",\n ")
|
||||
}
|
||||
|
||||
if strings.Contains(col.Name, "(") || strings.Contains(col.Name, " ") {
|
||||
// most likely an expression
|
||||
str.WriteString(trimmedColName)
|
||||
} else {
|
||||
// regular identifier
|
||||
str.WriteString("`")
|
||||
str.WriteString(trimmedColName)
|
||||
str.WriteString("`")
|
||||
}
|
||||
|
||||
if col.Collate != "" {
|
||||
str.WriteString(" COLLATE ")
|
||||
str.WriteString(col.Collate)
|
||||
}
|
||||
|
||||
if col.Sort != "" {
|
||||
str.WriteString(" ")
|
||||
str.WriteString(strings.ToUpper(col.Sort))
|
||||
}
|
||||
|
||||
hasCol = true
|
||||
}
|
||||
|
||||
if hasCol && len(idx.Columns) > 1 {
|
||||
str.WriteString("\n")
|
||||
}
|
||||
|
||||
str.WriteString(")")
|
||||
|
||||
if idx.Where != "" {
|
||||
str.WriteString(" WHERE ")
|
||||
str.WriteString(idx.Where)
|
||||
}
|
||||
|
||||
return str.String()
|
||||
}
|
||||
|
||||
// ParseIndex parses the provided "CREATE INDEX" SQL string into Index struct.
|
||||
func ParseIndex(createIndexExpr string) Index {
|
||||
result := Index{}
|
||||
|
||||
matches := indexRegex.FindStringSubmatch(createIndexExpr)
|
||||
if len(matches) != 7 {
|
||||
return result
|
||||
}
|
||||
|
||||
trimChars := "`\"'[]\r\n\t\f\v "
|
||||
|
||||
// Unique
|
||||
// ---
|
||||
result.Unique = strings.TrimSpace(matches[1]) != ""
|
||||
|
||||
// Optional (aka. "IF NOT EXISTS")
|
||||
// ---
|
||||
result.Optional = strings.TrimSpace(matches[2]) != ""
|
||||
|
||||
// SchemaName and IndexName
|
||||
// ---
|
||||
nameTk := tokenizer.NewFromString(matches[3])
|
||||
nameTk.Separators('.')
|
||||
|
||||
nameParts, _ := nameTk.ScanAll()
|
||||
if len(nameParts) == 2 {
|
||||
result.SchemaName = strings.Trim(nameParts[0], trimChars)
|
||||
result.IndexName = strings.Trim(nameParts[1], trimChars)
|
||||
} else {
|
||||
result.IndexName = strings.Trim(nameParts[0], trimChars)
|
||||
}
|
||||
|
||||
// TableName
|
||||
// ---
|
||||
result.TableName = strings.Trim(matches[4], trimChars)
|
||||
|
||||
// Columns
|
||||
// ---
|
||||
columnsTk := tokenizer.NewFromString(matches[5])
|
||||
columnsTk.Separators(',')
|
||||
|
||||
rawColumns, _ := columnsTk.ScanAll()
|
||||
|
||||
result.Columns = make([]IndexColumn, 0, len(rawColumns))
|
||||
|
||||
for _, col := range rawColumns {
|
||||
colMatches := indexColumnRegex.FindStringSubmatch(col)
|
||||
if len(colMatches) != 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
trimmedName := strings.Trim(colMatches[1], trimChars)
|
||||
if trimmedName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
result.Columns = append(result.Columns, IndexColumn{
|
||||
Name: trimmedName,
|
||||
Collate: strings.TrimSpace(colMatches[2]),
|
||||
Sort: strings.ToUpper(colMatches[3]),
|
||||
})
|
||||
}
|
||||
|
||||
// WHERE expression
|
||||
// ---
|
||||
result.Where = strings.TrimSpace(matches[6])
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package dbutils_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/dbutils"
|
||||
)
|
||||
|
||||
func TestParseIndex(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
index string
|
||||
expected dbutils.Index
|
||||
}{
|
||||
// invalid
|
||||
{
|
||||
`invalid`,
|
||||
dbutils.Index{},
|
||||
},
|
||||
// simple
|
||||
{
|
||||
`create index indexname on tablename (col1)`,
|
||||
dbutils.Index{
|
||||
IndexName: "indexname",
|
||||
TableName: "tablename",
|
||||
Columns: []dbutils.IndexColumn{
|
||||
{Name: "col1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
// all fields
|
||||
{
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS "schemaname".[indexname] on 'tablename' (
|
||||
col0,
|
||||
` + "`" + `col1` + "`" + `,
|
||||
json_extract("col2", "$.a") asc,
|
||||
"col3" collate NOCASE,
|
||||
"col4" collate RTRIM desc
|
||||
) where test = 1`,
|
||||
dbutils.Index{
|
||||
Unique: true,
|
||||
Optional: true,
|
||||
SchemaName: "schemaname",
|
||||
IndexName: "indexname",
|
||||
TableName: "tablename",
|
||||
Columns: []dbutils.IndexColumn{
|
||||
{Name: "col0"},
|
||||
{Name: "col1"},
|
||||
{Name: `json_extract("col2", "$.a")`, Sort: "ASC"},
|
||||
{Name: `col3`, Collate: "NOCASE"},
|
||||
{Name: `col4`, Collate: "RTRIM", Sort: "DESC"},
|
||||
},
|
||||
Where: "test = 1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
result := dbutils.ParseIndex(s.index)
|
||||
|
||||
resultRaw, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatalf("[%d] %v", i, err)
|
||||
}
|
||||
|
||||
expectedRaw, err := json.Marshal(s.expected)
|
||||
if err != nil {
|
||||
t.Fatalf("[%d] %v", i, err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(resultRaw, expectedRaw) {
|
||||
t.Errorf("[%d] Expected \n%s \ngot \n%s", i, expectedRaw, resultRaw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexIsValid(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
index dbutils.Index
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
"empty",
|
||||
dbutils.Index{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"no index name",
|
||||
dbutils.Index{
|
||||
TableName: "table",
|
||||
Columns: []dbutils.IndexColumn{{Name: "col"}},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"no table name",
|
||||
dbutils.Index{
|
||||
IndexName: "index",
|
||||
Columns: []dbutils.IndexColumn{{Name: "col"}},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"no columns",
|
||||
dbutils.Index{
|
||||
IndexName: "index",
|
||||
TableName: "table",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"min valid",
|
||||
dbutils.Index{
|
||||
IndexName: "index",
|
||||
TableName: "table",
|
||||
Columns: []dbutils.IndexColumn{{Name: "col"}},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"all fields",
|
||||
dbutils.Index{
|
||||
Optional: true,
|
||||
Unique: true,
|
||||
SchemaName: "schema",
|
||||
IndexName: "index",
|
||||
TableName: "table",
|
||||
Columns: []dbutils.IndexColumn{{Name: "col"}},
|
||||
Where: "test = 1 OR test = 2",
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
result := s.index.IsValid()
|
||||
|
||||
if result != s.expected {
|
||||
t.Errorf("[%s] Expected %v, got %v", s.name, s.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexBuild(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
index dbutils.Index
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"empty",
|
||||
dbutils.Index{},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"no index name",
|
||||
dbutils.Index{
|
||||
TableName: "table",
|
||||
Columns: []dbutils.IndexColumn{{Name: "col"}},
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"no table name",
|
||||
dbutils.Index{
|
||||
IndexName: "index",
|
||||
Columns: []dbutils.IndexColumn{{Name: "col"}},
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"no columns",
|
||||
dbutils.Index{
|
||||
IndexName: "index",
|
||||
TableName: "table",
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"min valid",
|
||||
dbutils.Index{
|
||||
IndexName: "index",
|
||||
TableName: "table",
|
||||
Columns: []dbutils.IndexColumn{{Name: "col"}},
|
||||
},
|
||||
"CREATE INDEX `index` ON `table` (`col`)",
|
||||
},
|
||||
{
|
||||
"all fields",
|
||||
dbutils.Index{
|
||||
Optional: true,
|
||||
Unique: true,
|
||||
SchemaName: "schema",
|
||||
IndexName: "index",
|
||||
TableName: "table",
|
||||
Columns: []dbutils.IndexColumn{
|
||||
{Name: "col1", Collate: "NOCASE", Sort: "asc"},
|
||||
{Name: "col2", Sort: "desc"},
|
||||
{Name: `json_extract("col3", "$.a")`, Collate: "NOCASE"},
|
||||
},
|
||||
Where: "test = 1 OR test = 2",
|
||||
},
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS `schema`.`index` ON `table` (\n `col1` COLLATE NOCASE ASC,\n `col2` DESC,\n " + `json_extract("col3", "$.a")` + " COLLATE NOCASE\n) WHERE test = 1 OR test = 2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
result := s.index.Build()
|
||||
|
||||
if result != s.expected {
|
||||
t.Errorf("[%s] Expected \n%v \ngot \n%v", s.name, s.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,7 +254,7 @@ func TestToUniqueStringSlice(t *testing.T) {
|
||||
{[]any{0, 1, "test", ""}, []string{"0", "1", "test"}},
|
||||
{[]string{"test1", "test2", "test1"}, []string{"test1", "test2"}},
|
||||
{`["test1", "test2", "test2"]`, []string{"test1", "test2"}},
|
||||
{types.JsonArray{"test1", "test2", "test1"}, []string{"test1", "test2"}},
|
||||
{types.JsonArray[string]{"test1", "test2", "test1"}, []string{"test1", "test2"}},
|
||||
}
|
||||
|
||||
for i, scenario := range scenarios {
|
||||
|
||||
@@ -107,6 +107,14 @@ func (r *Runner) Run(args ...string) error {
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
case "history-sync":
|
||||
if err := r.removeMissingAppliedMigrations(); err != nil {
|
||||
color.Red(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
color.Green("The %s table was synced with the available migrations.", r.tableName)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("Unsupported command: %q\n", cmd)
|
||||
@@ -252,3 +260,18 @@ func (r *Runner) lastAppliedMigrations(limit int) ([]string, error) {
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (r *Runner) removeMissingAppliedMigrations() error {
|
||||
loadedMigrations := r.migrationsList.Items()
|
||||
|
||||
names := make([]any, len(loadedMigrations))
|
||||
for i, migration := range loadedMigrations {
|
||||
names[i] = migration.File
|
||||
}
|
||||
|
||||
_, err := r.db.Delete(r.tableName, dbx.Not(dbx.HashExp{
|
||||
"file": names,
|
||||
})).Execute()
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -138,6 +138,56 @@ func TestRunnerUpAndDown(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHistorySync(t *testing.T) {
|
||||
testDB, err := createTestDB()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer testDB.Close()
|
||||
|
||||
// mock migrations history
|
||||
l := MigrationsList{}
|
||||
l.Register(func(db dbx.Builder) error {
|
||||
return nil
|
||||
}, func(db dbx.Builder) error {
|
||||
return nil
|
||||
}, "1_test")
|
||||
l.Register(func(db dbx.Builder) error {
|
||||
return nil
|
||||
}, func(db dbx.Builder) error {
|
||||
return nil
|
||||
}, "2_test")
|
||||
l.Register(func(db dbx.Builder) error {
|
||||
return nil
|
||||
}, func(db dbx.Builder) error {
|
||||
return nil
|
||||
}, "3_test")
|
||||
|
||||
r, err := NewRunner(testDB.DB, l)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize the runner: %v", err)
|
||||
}
|
||||
|
||||
if _, err := r.Up(); err != nil {
|
||||
t.Fatalf("Failed to apply the mock migrations: %v", err)
|
||||
}
|
||||
|
||||
if !r.isMigrationApplied(testDB.DB, "2_test") {
|
||||
t.Fatalf("Expected 2_test migration to be applied")
|
||||
}
|
||||
|
||||
// mock deleted migrations
|
||||
r.migrationsList.list = []*Migration{r.migrationsList.list[0], r.migrationsList.list[2]}
|
||||
|
||||
if err := r.removeMissingAppliedMigrations(); err != nil {
|
||||
t.Fatalf("Failed to remove missing applied migrations: %v", err)
|
||||
}
|
||||
|
||||
if r.isMigrationApplied(testDB.DB, "2_test") {
|
||||
t.Fatalf("Expected 2_test migration to NOT be applied")
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
+22
-2
@@ -107,9 +107,15 @@ func buildExpr(
|
||||
|
||||
switch op {
|
||||
case fexpr.SignEq, fexpr.SignAnyEq:
|
||||
expr = dbx.NewExp(fmt.Sprintf("COALESCE(%s, '') = COALESCE(%s, '')", left.Identifier, right.Identifier), mergeParams(left.Params, right.Params))
|
||||
expr = dbx.NewExp(
|
||||
fmt.Sprintf("%s = %s", normalizeNullIdentifier(left), normalizeNullIdentifier(right)),
|
||||
mergeParams(left.Params, right.Params),
|
||||
)
|
||||
case fexpr.SignNeq, fexpr.SignAnyNeq:
|
||||
expr = dbx.NewExp(fmt.Sprintf("COALESCE(%s, '') != COALESCE(%s, '')", left.Identifier, right.Identifier), mergeParams(left.Params, right.Params))
|
||||
expr = dbx.NewExp(
|
||||
fmt.Sprintf("%s != %s", normalizeNullIdentifier(left), normalizeNullIdentifier(right)),
|
||||
mergeParams(left.Params, right.Params),
|
||||
)
|
||||
case fexpr.SignLike, fexpr.SignAnyLike:
|
||||
// the right side is a column and therefor wrap it with "%" for contains like behavior
|
||||
if len(right.Params) == 0 {
|
||||
@@ -232,6 +238,20 @@ func resolveToken(token fexpr.Token, fieldResolver FieldResolver) (*ResolverResu
|
||||
return nil, errors.New("unresolvable token type")
|
||||
}
|
||||
|
||||
func normalizeNullIdentifier(result *ResolverResult) string {
|
||||
lower := strings.ToLower(result.Identifier)
|
||||
|
||||
if lower == "null" {
|
||||
return "''"
|
||||
}
|
||||
|
||||
if strings.Contains(lower, "json_extract(") || strings.Contains(lower, "json_array_length(") {
|
||||
return fmt.Sprintf("COALESCE(%s, '')", result.Identifier)
|
||||
}
|
||||
|
||||
return result.Identifier
|
||||
}
|
||||
|
||||
func isAnyMatchOp(op fexpr.SignOp) bool {
|
||||
switch op {
|
||||
case
|
||||
|
||||
+21
-13
@@ -40,7 +40,8 @@ func TestFilterDataBuildExpr(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"simple expression",
|
||||
"test1 > 1", false,
|
||||
"test1 > 1",
|
||||
false,
|
||||
"^" +
|
||||
regexp.QuoteMeta("[[test1]] > {:") +
|
||||
".+" +
|
||||
@@ -49,14 +50,16 @@ func TestFilterDataBuildExpr(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"like with 2 columns",
|
||||
"test1 ~ test2", false,
|
||||
"test1 ~ test2",
|
||||
false,
|
||||
"^" +
|
||||
regexp.QuoteMeta("[[test1]] LIKE ('%' || [[test2]] || '%') ESCAPE '\\'") +
|
||||
"$",
|
||||
},
|
||||
{
|
||||
"like with right column operand",
|
||||
"'lorem' ~ test1", false,
|
||||
"'lorem' ~ test1",
|
||||
false,
|
||||
"^" +
|
||||
regexp.QuoteMeta("{:") +
|
||||
".+" +
|
||||
@@ -65,7 +68,8 @@ func TestFilterDataBuildExpr(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"like with left column operand and text as right operand",
|
||||
"test1 ~ 'lorem'", false,
|
||||
"test1 ~ 'lorem'",
|
||||
false,
|
||||
"^" +
|
||||
regexp.QuoteMeta("[[test1]] LIKE {:") +
|
||||
".+" +
|
||||
@@ -74,14 +78,16 @@ func TestFilterDataBuildExpr(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"not like with 2 columns",
|
||||
"test1 !~ test2", false,
|
||||
"test1 !~ test2",
|
||||
false,
|
||||
"^" +
|
||||
regexp.QuoteMeta("[[test1]] NOT LIKE ('%' || [[test2]] || '%') ESCAPE '\\'") +
|
||||
"$",
|
||||
},
|
||||
{
|
||||
"not like with right column operand",
|
||||
"'lorem' !~ test1", false,
|
||||
"'lorem' !~ test1",
|
||||
false,
|
||||
"^" +
|
||||
regexp.QuoteMeta("{:") +
|
||||
".+" +
|
||||
@@ -90,7 +96,8 @@ func TestFilterDataBuildExpr(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"like with left column operand and text as right operand",
|
||||
"test1 !~ 'lorem'", false,
|
||||
"test1 !~ 'lorem'",
|
||||
false,
|
||||
"^" +
|
||||
regexp.QuoteMeta("[[test1]] NOT LIKE {:") +
|
||||
".+" +
|
||||
@@ -99,7 +106,8 @@ func TestFilterDataBuildExpr(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"current datetime constant",
|
||||
"test1 > @now", false,
|
||||
"test1 > @now",
|
||||
false,
|
||||
"^" +
|
||||
regexp.QuoteMeta("[[test1]] > {:") +
|
||||
".+" +
|
||||
@@ -113,25 +121,25 @@ func TestFilterDataBuildExpr(t *testing.T) {
|
||||
"^" +
|
||||
regexp.QuoteMeta("(([[test1]] > {:") +
|
||||
".+" +
|
||||
regexp.QuoteMeta("} OR COALESCE([[test2]], '') != COALESCE({:") +
|
||||
regexp.QuoteMeta("} OR [[test2]] != {:") +
|
||||
".+" +
|
||||
regexp.QuoteMeta("}, '')) AND [[test3]] LIKE {:") +
|
||||
regexp.QuoteMeta("}) AND [[test3]] LIKE {:") +
|
||||
".+" +
|
||||
regexp.QuoteMeta("} ESCAPE '\\' AND COALESCE([[test4.sub]], '') = COALESCE(NULL, ''))") +
|
||||
regexp.QuoteMeta("} ESCAPE '\\' AND [[test4.sub]] = '')") +
|
||||
"$",
|
||||
},
|
||||
{
|
||||
"combination of special literals (null, true, false)",
|
||||
"test1=true && test2 != false && test3 = null || test4.sub != null",
|
||||
false,
|
||||
"^" + regexp.QuoteMeta("(COALESCE([[test1]], '') = COALESCE(1, '') AND COALESCE([[test2]], '') != COALESCE(0, '') AND COALESCE([[test3]], '') = COALESCE(NULL, '') OR COALESCE([[test4.sub]], '') != COALESCE(NULL, ''))") + "$",
|
||||
"^" + regexp.QuoteMeta("([[test1]] = 1 AND [[test2]] != 0 AND [[test3]] = '' OR [[test4.sub]] != '')") + "$",
|
||||
},
|
||||
{
|
||||
"all operators",
|
||||
"(test1 = test2 || test2 != test3) && (test2 ~ 'example' || test2 !~ '%%abc') && 'switch1%%' ~ test1 && 'switch2' !~ test2 && test3 > 1 && test3 >= 0 && test3 <= 4 && 2 < 5",
|
||||
false,
|
||||
"^" +
|
||||
regexp.QuoteMeta("((COALESCE([[test1]], '') = COALESCE([[test2]], '') OR COALESCE([[test2]], '') != COALESCE([[test3]], '')) AND ([[test2]] LIKE {:") +
|
||||
regexp.QuoteMeta("(([[test1]] = [[test2]] OR [[test2]] != [[test3]]) AND ([[test2]] LIKE {:") +
|
||||
".+" +
|
||||
regexp.QuoteMeta("} ESCAPE '\\' OR [[test2]] NOT LIKE {:") +
|
||||
".+" +
|
||||
|
||||
@@ -274,8 +274,8 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
|
||||
false,
|
||||
`{"page":1,"perPage":` + fmt.Sprint(MaxPerPage) + `,"totalItems":1,"totalPages":1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
|
||||
[]string{
|
||||
"SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= 2)",
|
||||
"SELECT * FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= 2) ORDER BY `test1` ASC, `test2` DESC LIMIT 500",
|
||||
"SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (test2 != '')) AND (test1 >= 2)",
|
||||
"SELECT * FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (test2 != '')) AND (test1 >= 2) ORDER BY `test1` ASC, `test2` DESC LIMIT 500",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -287,8 +287,8 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
|
||||
false,
|
||||
`{"page":1,"perPage":10,"totalItems":0,"totalPages":0,"items":[]}`,
|
||||
[]string{
|
||||
"SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', ''))",
|
||||
"SELECT * FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', '')) ORDER BY `test1` ASC, `test3` ASC LIMIT 10",
|
||||
"SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE (NOT (`test1` IS NULL)) AND (test3 != '')",
|
||||
"SELECT * FROM `test` WHERE (NOT (`test1` IS NULL)) AND (test3 != '') ORDER BY `test1` ASC, `test3` ASC LIMIT 10",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,30 +7,31 @@ import (
|
||||
)
|
||||
|
||||
// JsonArray defines a slice that is safe for json and db read/write.
|
||||
type JsonArray []any
|
||||
type JsonArray[T any] []T
|
||||
|
||||
// internal alias to prevent recursion during marshalization.
|
||||
type jsonArrayAlias[T any] JsonArray[T]
|
||||
|
||||
// MarshalJSON implements the [json.Marshaler] interface.
|
||||
func (m JsonArray) MarshalJSON() ([]byte, error) {
|
||||
type alias JsonArray // prevent recursion
|
||||
|
||||
func (m JsonArray[T]) MarshalJSON() ([]byte, error) {
|
||||
// initialize an empty map to ensure that `[]` is returned as json
|
||||
if m == nil {
|
||||
m = JsonArray{}
|
||||
m = JsonArray[T]{}
|
||||
}
|
||||
|
||||
return json.Marshal(alias(m))
|
||||
return json.Marshal(jsonArrayAlias[T](m))
|
||||
}
|
||||
|
||||
// Value implements the [driver.Valuer] interface.
|
||||
func (m JsonArray) Value() (driver.Value, error) {
|
||||
func (m JsonArray[T]) Value() (driver.Value, error) {
|
||||
data, err := json.Marshal(m)
|
||||
|
||||
return string(data), err
|
||||
}
|
||||
|
||||
// Scan implements [sql.Scanner] interface to scan the provided value
|
||||
// into the current `JsonArray` instance.
|
||||
func (m *JsonArray) Scan(value any) error {
|
||||
// into the current JsonArray[T] instance.
|
||||
func (m *JsonArray[T]) Scan(value any) error {
|
||||
var data []byte
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
|
||||
@@ -2,6 +2,7 @@ package types_test
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
@@ -9,14 +10,14 @@ import (
|
||||
|
||||
func TestJsonArrayMarshalJSON(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
json types.JsonArray
|
||||
json json.Marshaler
|
||||
expected string
|
||||
}{
|
||||
{nil, "[]"},
|
||||
{types.JsonArray{}, `[]`},
|
||||
{types.JsonArray{1, 2, 3}, `[1,2,3]`},
|
||||
{types.JsonArray{"test1", "test2", "test3"}, `["test1","test2","test3"]`},
|
||||
{types.JsonArray{1, "test"}, `[1,"test"]`},
|
||||
{new(types.JsonArray[any]), "[]"},
|
||||
{types.JsonArray[any]{}, `[]`},
|
||||
{types.JsonArray[int]{1, 2, 3}, `[1,2,3]`},
|
||||
{types.JsonArray[string]{"test1", "test2", "test3"}, `["test1","test2","test3"]`},
|
||||
{types.JsonArray[any]{1, "test"}, `[1,"test"]`},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
@@ -33,14 +34,14 @@ func TestJsonArrayMarshalJSON(t *testing.T) {
|
||||
|
||||
func TestJsonArrayValue(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
json types.JsonArray
|
||||
json driver.Valuer
|
||||
expected driver.Value
|
||||
}{
|
||||
{nil, `[]`},
|
||||
{types.JsonArray{}, `[]`},
|
||||
{types.JsonArray{1, 2, 3}, `[1,2,3]`},
|
||||
{types.JsonArray{"test1", "test2", "test3"}, `["test1","test2","test3"]`},
|
||||
{types.JsonArray{1, "test"}, `[1,"test"]`},
|
||||
{new(types.JsonArray[any]), `[]`},
|
||||
{types.JsonArray[any]{}, `[]`},
|
||||
{types.JsonArray[int]{1, 2, 3}, `[1,2,3]`},
|
||||
{types.JsonArray[string]{"test1", "test2", "test3"}, `["test1","test2","test3"]`},
|
||||
{types.JsonArray[any]{1, "test"}, `[1,"test"]`},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
@@ -77,7 +78,7 @@ func TestJsonArrayScan(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
arr := types.JsonArray{}
|
||||
arr := types.JsonArray[any]{}
|
||||
scanErr := arr.Scan(s.value)
|
||||
|
||||
hasErr := scanErr != nil
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
const { exec } = require('node:child_process');
|
||||
|
||||
// you can use any other library for copying directories recursively
|
||||
const fse = require('fs-extra');
|
||||
|
||||
let controller; // this will be used to terminate the PocketBase process
|
||||
|
||||
const srcTestDirPath = "./test_pb_data";
|
||||
const tempTestDirPath = "./temp_test_pb_data";
|
||||
|
||||
beforeEach(() => {
|
||||
// copy test_pb_date to a temp location
|
||||
fse.copySync(srcTestDirPath, tempTestDirPath);
|
||||
|
||||
controller = new AbortController();
|
||||
|
||||
// start PocketBase with the test_pb_data
|
||||
exec('./pocketbase serve --dir=' + tempTestDirPath, { signal: controller.signal});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// stop the PocketBase process
|
||||
controller.abort();
|
||||
|
||||
// clean up the temp test directory
|
||||
fse.removeSync(tempTestDirPath);
|
||||
});
|
||||
@@ -8,4 +8,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.13.3"
|
||||
PB_VERSION = "v0.14.0"
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import{S as ke,i as be,s as ge,e as r,w as b,b as g,c as me,f as k,g as h,h as n,m as _e,x as G,O as re,P as we,k as ve,Q as Ce,n as Pe,t as L,a as Y,o as m,d as pe,R as Me,C as Se,p as $e,r as H,u as je,N as Ae}from"./index-4bd199fb.js";import{S as Be}from"./SdkTabs-fc1a80c5.js";function ue(a,l,o){const s=a.slice();return s[5]=l[o],s}function de(a,l,o){const s=a.slice();return s[5]=l[o],s}function fe(a,l){let o,s=l[5].code+"",_,f,i,u;function d(){return l[4](l[5])}return{key:a,first:null,c(){o=r("button"),_=b(s),f=g(),k(o,"class","tab-item"),H(o,"active",l[1]===l[5].code),this.first=o},m(v,C){h(v,o,C),n(o,_),n(o,f),i||(u=je(o,"click",d),i=!0)},p(v,C){l=v,C&4&&s!==(s=l[5].code+"")&&G(_,s),C&6&&H(o,"active",l[1]===l[5].code)},d(v){v&&m(o),i=!1,u()}}}function he(a,l){let o,s,_,f;return s=new Ae({props:{content:l[5].body}}),{key:a,first:null,c(){o=r("div"),me(s.$$.fragment),_=g(),k(o,"class","tab-item"),H(o,"active",l[1]===l[5].code),this.first=o},m(i,u){h(i,o,u),_e(s,o,null),n(o,_),f=!0},p(i,u){l=i;const d={};u&4&&(d.content=l[5].body),s.$set(d),(!f||u&6)&&H(o,"active",l[1]===l[5].code)},i(i){f||(L(s.$$.fragment,i),f=!0)},o(i){Y(s.$$.fragment,i),f=!1},d(i){i&&m(o),pe(s)}}}function Oe(a){var ae,ne;let l,o,s=a[0].name+"",_,f,i,u,d,v,C,F=a[0].name+"",U,R,q,P,D,j,W,M,K,X,Q,A,Z,V,y=a[0].name+"",E,x,I,B,J,S,O,w=[],ee=new Map,te,T,p=[],le=new Map,$;P=new Be({props:{js:`
|
||||
import{S as ke,i as be,s as ge,e as r,w as g,b as w,c as me,f as k,g as h,h as n,m as _e,x as G,O as re,P as we,k as ve,Q as Ce,n as Pe,t as L,a as Y,o as m,d as pe,R as Me,C as Se,p as $e,r as H,u as je,N as Ae}from"./index-ea3aeb4c.js";import{S as Be}from"./SdkTabs-27c932bb.js";function ue(a,l,o){const s=a.slice();return s[5]=l[o],s}function de(a,l,o){const s=a.slice();return s[5]=l[o],s}function fe(a,l){let o,s=l[5].code+"",_,f,i,u;function d(){return l[4](l[5])}return{key:a,first:null,c(){o=r("button"),_=g(s),f=w(),k(o,"class","tab-item"),H(o,"active",l[1]===l[5].code),this.first=o},m(v,C){h(v,o,C),n(o,_),n(o,f),i||(u=je(o,"click",d),i=!0)},p(v,C){l=v,C&4&&s!==(s=l[5].code+"")&&G(_,s),C&6&&H(o,"active",l[1]===l[5].code)},d(v){v&&m(o),i=!1,u()}}}function he(a,l){let o,s,_,f;return s=new Ae({props:{content:l[5].body}}),{key:a,first:null,c(){o=r("div"),me(s.$$.fragment),_=w(),k(o,"class","tab-item"),H(o,"active",l[1]===l[5].code),this.first=o},m(i,u){h(i,o,u),_e(s,o,null),n(o,_),f=!0},p(i,u){l=i;const d={};u&4&&(d.content=l[5].body),s.$set(d),(!f||u&6)&&H(o,"active",l[1]===l[5].code)},i(i){f||(L(s.$$.fragment,i),f=!0)},o(i){Y(s.$$.fragment,i),f=!1},d(i){i&&m(o),pe(s)}}}function Oe(a){var ae,ne;let l,o,s=a[0].name+"",_,f,i,u,d,v,C,F=a[0].name+"",U,R,q,P,D,j,W,M,K,X,Q,A,Z,V,y=a[0].name+"",E,x,I,B,J,S,O,b=[],ee=new Map,te,T,p=[],le=new Map,$;P=new Be({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${a[3]}');
|
||||
@@ -14,7 +14,7 @@ import{S as ke,i as be,s as ge,e as r,w as b,b as g,c as me,f as k,g as h,h as n
|
||||
...
|
||||
|
||||
final result = await pb.collection('${(ne=a[0])==null?void 0:ne.name}').listAuthMethods();
|
||||
`}});let z=a[2];const oe=e=>e[5].code;for(let e=0;e<z.length;e+=1){let t=de(a,z,e),c=oe(t);ee.set(c,w[e]=fe(c,t))}let N=a[2];const se=e=>e[5].code;for(let e=0;e<N.length;e+=1){let t=ue(a,N,e),c=se(t);le.set(c,p[e]=he(c,t))}return{c(){l=r("h3"),o=b("List auth methods ("),_=b(s),f=b(")"),i=g(),u=r("div"),d=r("p"),v=b("Returns a public list with all allowed "),C=r("strong"),U=b(F),R=b(" authentication methods."),q=g(),me(P.$$.fragment),D=g(),j=r("h6"),j.textContent="API details",W=g(),M=r("div"),K=r("strong"),K.textContent="GET",X=g(),Q=r("div"),A=r("p"),Z=b("/api/collections/"),V=r("strong"),E=b(y),x=b("/auth-methods"),I=g(),B=r("div"),B.textContent="Responses",J=g(),S=r("div"),O=r("div");for(let e=0;e<w.length;e+=1)w[e].c();te=g(),T=r("div");for(let e=0;e<p.length;e+=1)p[e].c();k(l,"class","m-b-sm"),k(u,"class","content txt-lg m-b-sm"),k(j,"class","m-b-xs"),k(K,"class","label label-primary"),k(Q,"class","content"),k(M,"class","alert alert-info"),k(B,"class","section-title"),k(O,"class","tabs-header compact left"),k(T,"class","tabs-content"),k(S,"class","tabs")},m(e,t){h(e,l,t),n(l,o),n(l,_),n(l,f),h(e,i,t),h(e,u,t),n(u,d),n(d,v),n(d,C),n(C,U),n(d,R),h(e,q,t),_e(P,e,t),h(e,D,t),h(e,j,t),h(e,W,t),h(e,M,t),n(M,K),n(M,X),n(M,Q),n(Q,A),n(A,Z),n(A,V),n(V,E),n(A,x),h(e,I,t),h(e,B,t),h(e,J,t),h(e,S,t),n(S,O);for(let c=0;c<w.length;c+=1)w[c].m(O,null);n(S,te),n(S,T);for(let c=0;c<p.length;c+=1)p[c].m(T,null);$=!0},p(e,[t]){var ie,ce;(!$||t&1)&&s!==(s=e[0].name+"")&&G(_,s),(!$||t&1)&&F!==(F=e[0].name+"")&&G(U,F);const c={};t&9&&(c.js=`
|
||||
`}});let z=a[2];const oe=e=>e[5].code;for(let e=0;e<z.length;e+=1){let t=de(a,z,e),c=oe(t);ee.set(c,b[e]=fe(c,t))}let N=a[2];const se=e=>e[5].code;for(let e=0;e<N.length;e+=1){let t=ue(a,N,e),c=se(t);le.set(c,p[e]=he(c,t))}return{c(){l=r("h3"),o=g("List auth methods ("),_=g(s),f=g(")"),i=w(),u=r("div"),d=r("p"),v=g("Returns a public list with all allowed "),C=r("strong"),U=g(F),R=g(" authentication methods."),q=w(),me(P.$$.fragment),D=w(),j=r("h6"),j.textContent="API details",W=w(),M=r("div"),K=r("strong"),K.textContent="GET",X=w(),Q=r("div"),A=r("p"),Z=g("/api/collections/"),V=r("strong"),E=g(y),x=g("/auth-methods"),I=w(),B=r("div"),B.textContent="Responses",J=w(),S=r("div"),O=r("div");for(let e=0;e<b.length;e+=1)b[e].c();te=w(),T=r("div");for(let e=0;e<p.length;e+=1)p[e].c();k(l,"class","m-b-sm"),k(u,"class","content txt-lg m-b-sm"),k(j,"class","m-b-xs"),k(K,"class","label label-primary"),k(Q,"class","content"),k(M,"class","alert alert-info"),k(B,"class","section-title"),k(O,"class","tabs-header compact left"),k(T,"class","tabs-content"),k(S,"class","tabs")},m(e,t){h(e,l,t),n(l,o),n(l,_),n(l,f),h(e,i,t),h(e,u,t),n(u,d),n(d,v),n(d,C),n(C,U),n(d,R),h(e,q,t),_e(P,e,t),h(e,D,t),h(e,j,t),h(e,W,t),h(e,M,t),n(M,K),n(M,X),n(M,Q),n(Q,A),n(A,Z),n(A,V),n(V,E),n(A,x),h(e,I,t),h(e,B,t),h(e,J,t),h(e,S,t),n(S,O);for(let c=0;c<b.length;c+=1)b[c]&&b[c].m(O,null);n(S,te),n(S,T);for(let c=0;c<p.length;c+=1)p[c]&&p[c].m(T,null);$=!0},p(e,[t]){var ie,ce;(!$||t&1)&&s!==(s=e[0].name+"")&&G(_,s),(!$||t&1)&&F!==(F=e[0].name+"")&&G(U,F);const c={};t&9&&(c.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${e[3]}');
|
||||
@@ -30,7 +30,7 @@ import{S as ke,i as be,s as ge,e as r,w as b,b as g,c as me,f as k,g as h,h as n
|
||||
...
|
||||
|
||||
final result = await pb.collection('${(ce=e[0])==null?void 0:ce.name}').listAuthMethods();
|
||||
`),P.$set(c),(!$||t&1)&&y!==(y=e[0].name+"")&&G(E,y),t&6&&(z=e[2],w=re(w,t,oe,1,e,z,ee,O,we,fe,null,de)),t&6&&(N=e[2],ve(),p=re(p,t,se,1,e,N,le,T,Ce,he,null,ue),Pe())},i(e){if(!$){L(P.$$.fragment,e);for(let t=0;t<N.length;t+=1)L(p[t]);$=!0}},o(e){Y(P.$$.fragment,e);for(let t=0;t<p.length;t+=1)Y(p[t]);$=!1},d(e){e&&m(l),e&&m(i),e&&m(u),e&&m(q),pe(P,e),e&&m(D),e&&m(j),e&&m(W),e&&m(M),e&&m(I),e&&m(B),e&&m(J),e&&m(S);for(let t=0;t<w.length;t+=1)w[t].d();for(let t=0;t<p.length;t+=1)p[t].d()}}}function Te(a,l,o){let s,{collection:_=new Me}=l,f=200,i=[];const u=d=>o(1,f=d.code);return a.$$set=d=>{"collection"in d&&o(0,_=d.collection)},o(3,s=Se.getApiExampleUrl($e.baseUrl)),o(2,i=[{code:200,body:`
|
||||
`),P.$set(c),(!$||t&1)&&y!==(y=e[0].name+"")&&G(E,y),t&6&&(z=e[2],b=re(b,t,oe,1,e,z,ee,O,we,fe,null,de)),t&6&&(N=e[2],ve(),p=re(p,t,se,1,e,N,le,T,Ce,he,null,ue),Pe())},i(e){if(!$){L(P.$$.fragment,e);for(let t=0;t<N.length;t+=1)L(p[t]);$=!0}},o(e){Y(P.$$.fragment,e);for(let t=0;t<p.length;t+=1)Y(p[t]);$=!1},d(e){e&&m(l),e&&m(i),e&&m(u),e&&m(q),pe(P,e),e&&m(D),e&&m(j),e&&m(W),e&&m(M),e&&m(I),e&&m(B),e&&m(J),e&&m(S);for(let t=0;t<b.length;t+=1)b[t].d();for(let t=0;t<p.length;t+=1)p[t].d()}}}function Te(a,l,o){let s,{collection:_=new Me}=l,f=200,i=[];const u=d=>o(1,f=d.code);return a.$$set=d=>{"collection"in d&&o(0,_=d.collection)},o(3,s=Se.getApiExampleUrl($e.baseUrl)),o(2,i=[{code:200,body:`
|
||||
{
|
||||
"usernamePassword": true,
|
||||
"emailPassword": true,
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
import{S as ze,i as Ue,s as je,N as Ve,e as a,w as k,b as p,c as ae,f as b,g as c,h as o,m as ne,x as re,O as qe,P as xe,k as Je,Q as Ke,n as Qe,t as U,a as j,o as d,d as ie,R as Ie,C as He,p as We,r as x,u as Ge}from"./index-4bd199fb.js";import{S as Xe}from"./SdkTabs-fc1a80c5.js";function Ee(r,l,s){const n=r.slice();return n[5]=l[s],n}function Fe(r,l,s){const n=r.slice();return n[5]=l[s],n}function Le(r,l){let s,n=l[5].code+"",m,_,i,f;function v(){return l[4](l[5])}return{key:r,first:null,c(){s=a("button"),m=k(n),_=p(),b(s,"class","tab-item"),x(s,"active",l[1]===l[5].code),this.first=s},m(g,w){c(g,s,w),o(s,m),o(s,_),i||(f=Ge(s,"click",v),i=!0)},p(g,w){l=g,w&4&&n!==(n=l[5].code+"")&&re(m,n),w&6&&x(s,"active",l[1]===l[5].code)},d(g){g&&d(s),i=!1,f()}}}function Ne(r,l){let s,n,m,_;return n=new Ve({props:{content:l[5].body}}),{key:r,first:null,c(){s=a("div"),ae(n.$$.fragment),m=p(),b(s,"class","tab-item"),x(s,"active",l[1]===l[5].code),this.first=s},m(i,f){c(i,s,f),ne(n,s,null),o(s,m),_=!0},p(i,f){l=i;const v={};f&4&&(v.content=l[5].body),n.$set(v),(!_||f&6)&&x(s,"active",l[1]===l[5].code)},i(i){_||(U(n.$$.fragment,i),_=!0)},o(i){j(n.$$.fragment,i),_=!1},d(i){i&&d(s),ie(n)}}}function Ye(r){var Be,Me;let l,s,n=r[0].name+"",m,_,i,f,v,g,w,B,J,S,F,ce,L,M,de,K,N=r[0].name+"",Q,ue,pe,V,I,D,W,T,G,fe,X,C,Y,he,Z,be,h,me,P,_e,ke,ve,ee,ge,te,ye,Se,$e,oe,we,le,O,se,R,q,$=[],Te=new Map,Ce,H,y=[],Re=new Map,A;g=new Xe({props:{js:`
|
||||
import{S as ze,i as Ue,s as je,N as Ve,e as a,w as k,b as p,c as ae,f as b,g as d,h as o,m as ne,x as re,O as qe,P as xe,k as Je,Q as Ke,n as Qe,t as U,a as j,o as u,d as ie,R as Ie,C as He,p as We,r as x,u as Ge}from"./index-ea3aeb4c.js";import{S as Xe}from"./SdkTabs-27c932bb.js";function Ee(r,l,s){const n=r.slice();return n[5]=l[s],n}function Fe(r,l,s){const n=r.slice();return n[5]=l[s],n}function Le(r,l){let s,n=l[5].code+"",m,_,i,f;function v(){return l[4](l[5])}return{key:r,first:null,c(){s=a("button"),m=k(n),_=p(),b(s,"class","tab-item"),x(s,"active",l[1]===l[5].code),this.first=s},m(g,w){d(g,s,w),o(s,m),o(s,_),i||(f=Ge(s,"click",v),i=!0)},p(g,w){l=g,w&4&&n!==(n=l[5].code+"")&&re(m,n),w&6&&x(s,"active",l[1]===l[5].code)},d(g){g&&u(s),i=!1,f()}}}function Ne(r,l){let s,n,m,_;return n=new Ve({props:{content:l[5].body}}),{key:r,first:null,c(){s=a("div"),ae(n.$$.fragment),m=p(),b(s,"class","tab-item"),x(s,"active",l[1]===l[5].code),this.first=s},m(i,f){d(i,s,f),ne(n,s,null),o(s,m),_=!0},p(i,f){l=i;const v={};f&4&&(v.content=l[5].body),n.$set(v),(!_||f&6)&&x(s,"active",l[1]===l[5].code)},i(i){_||(U(n.$$.fragment,i),_=!0)},o(i){j(n.$$.fragment,i),_=!1},d(i){i&&u(s),ie(n)}}}function Ye(r){var Be,Me;let l,s,n=r[0].name+"",m,_,i,f,v,g,w,B,J,$,F,ce,L,M,de,K,N=r[0].name+"",Q,ue,pe,V,I,D,W,T,G,fe,X,C,Y,he,Z,be,h,me,P,_e,ke,ve,ee,ge,te,ye,Se,$e,oe,we,le,O,se,R,q,S=[],Te=new Map,Ce,H,y=[],Re=new Map,A;g=new Xe({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${r[3]}');
|
||||
@@ -24,10 +24,10 @@ import{S as ze,i as Ue,s as je,N as Ve,e as a,w as k,b as p,c as ae,f as b,g as
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.model.id);
|
||||
`}}),P=new Ve({props:{content:"?expand=relField1,relField2.subRelField"}});let z=r[2];const Pe=e=>e[5].code;for(let e=0;e<z.length;e+=1){let t=Fe(r,z,e),u=Pe(t);Te.set(u,$[e]=Le(u,t))}let E=r[2];const Ae=e=>e[5].code;for(let e=0;e<E.length;e+=1){let t=Ee(r,E,e),u=Ae(t);Re.set(u,y[e]=Ne(u,t))}return{c(){l=a("h3"),s=k("Auth refresh ("),m=k(n),_=k(")"),i=p(),f=a("div"),f.innerHTML=`<p>Returns a new auth response (token and record data) for an
|
||||
`}}),P=new Ve({props:{content:"?expand=relField1,relField2.subRelField"}});let z=r[2];const Pe=e=>e[5].code;for(let e=0;e<z.length;e+=1){let t=Fe(r,z,e),c=Pe(t);Te.set(c,S[e]=Le(c,t))}let E=r[2];const Ae=e=>e[5].code;for(let e=0;e<E.length;e+=1){let t=Ee(r,E,e),c=Ae(t);Re.set(c,y[e]=Ne(c,t))}return{c(){l=a("h3"),s=k("Auth refresh ("),m=k(n),_=k(")"),i=p(),f=a("div"),f.innerHTML=`<p>Returns a new auth response (token and record data) for an
|
||||
<strong>already authenticated record</strong>.</p>
|
||||
<p><em>This method is usually called by users on page/screen reload to ensure that the previously stored
|
||||
data in <code>pb.authStore</code> is still valid and up-to-date.</em></p>`,v=p(),ae(g.$$.fragment),w=p(),B=a("h6"),B.textContent="API details",J=p(),S=a("div"),F=a("strong"),F.textContent="POST",ce=p(),L=a("div"),M=a("p"),de=k("/api/collections/"),K=a("strong"),Q=k(N),ue=k("/auth-refresh"),pe=p(),V=a("p"),V.innerHTML="Requires record <code>Authorization:TOKEN</code> header",I=p(),D=a("div"),D.textContent="Query parameters",W=p(),T=a("table"),G=a("thead"),G.innerHTML=`<tr><th>Param</th>
|
||||
data in <code>pb.authStore</code> is still valid and up-to-date.</em></p>`,v=p(),ae(g.$$.fragment),w=p(),B=a("h6"),B.textContent="API details",J=p(),$=a("div"),F=a("strong"),F.textContent="POST",ce=p(),L=a("div"),M=a("p"),de=k("/api/collections/"),K=a("strong"),Q=k(N),ue=k("/auth-refresh"),pe=p(),V=a("p"),V.innerHTML="Requires record <code>Authorization:TOKEN</code> header",I=p(),D=a("div"),D.textContent="Query parameters",W=p(),T=a("table"),G=a("thead"),G.innerHTML=`<tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th></tr>`,fe=p(),X=a("tbody"),C=a("tr"),Y=a("td"),Y.textContent="expand",he=p(),Z=a("td"),Z.innerHTML='<span class="label">String</span>',be=p(),h=a("td"),me=k(`Auto expand record relations. Ex.:
|
||||
`),ae(P.$$.fragment),_e=k(`
|
||||
@@ -35,7 +35,7 @@ import{S as ze,i as Ue,s as je,N as Ve,e as a,w as k,b as p,c as ae,f as b,g as
|
||||
The expanded relations will be appended to the record under the
|
||||
`),ee=a("code"),ee.textContent="expand",ge=k(" property (eg. "),te=a("code"),te.textContent='"expand": {"relField1": {...}, ...}',ye=k(`).
|
||||
`),Se=a("br"),$e=k(`
|
||||
Only the relations to which the request user has permissions to `),oe=a("strong"),oe.textContent="view",we=k(" will be expanded."),le=p(),O=a("div"),O.textContent="Responses",se=p(),R=a("div"),q=a("div");for(let e=0;e<$.length;e+=1)$[e].c();Ce=p(),H=a("div");for(let e=0;e<y.length;e+=1)y[e].c();b(l,"class","m-b-sm"),b(f,"class","content txt-lg m-b-sm"),b(B,"class","m-b-xs"),b(F,"class","label label-primary"),b(L,"class","content"),b(V,"class","txt-hint txt-sm txt-right"),b(S,"class","alert alert-success"),b(D,"class","section-title"),b(T,"class","table-compact table-border m-b-base"),b(O,"class","section-title"),b(q,"class","tabs-header compact left"),b(H,"class","tabs-content"),b(R,"class","tabs")},m(e,t){c(e,l,t),o(l,s),o(l,m),o(l,_),c(e,i,t),c(e,f,t),c(e,v,t),ne(g,e,t),c(e,w,t),c(e,B,t),c(e,J,t),c(e,S,t),o(S,F),o(S,ce),o(S,L),o(L,M),o(M,de),o(M,K),o(K,Q),o(M,ue),o(S,pe),o(S,V),c(e,I,t),c(e,D,t),c(e,W,t),c(e,T,t),o(T,G),o(T,fe),o(T,X),o(X,C),o(C,Y),o(C,he),o(C,Z),o(C,be),o(C,h),o(h,me),ne(P,h,null),o(h,_e),o(h,ke),o(h,ve),o(h,ee),o(h,ge),o(h,te),o(h,ye),o(h,Se),o(h,$e),o(h,oe),o(h,we),c(e,le,t),c(e,O,t),c(e,se,t),c(e,R,t),o(R,q);for(let u=0;u<$.length;u+=1)$[u].m(q,null);o(R,Ce),o(R,H);for(let u=0;u<y.length;u+=1)y[u].m(H,null);A=!0},p(e,[t]){var De,Oe;(!A||t&1)&&n!==(n=e[0].name+"")&&re(m,n);const u={};t&9&&(u.js=`
|
||||
Only the relations to which the request user has permissions to `),oe=a("strong"),oe.textContent="view",we=k(" will be expanded."),le=p(),O=a("div"),O.textContent="Responses",se=p(),R=a("div"),q=a("div");for(let e=0;e<S.length;e+=1)S[e].c();Ce=p(),H=a("div");for(let e=0;e<y.length;e+=1)y[e].c();b(l,"class","m-b-sm"),b(f,"class","content txt-lg m-b-sm"),b(B,"class","m-b-xs"),b(F,"class","label label-primary"),b(L,"class","content"),b(V,"class","txt-hint txt-sm txt-right"),b($,"class","alert alert-success"),b(D,"class","section-title"),b(T,"class","table-compact table-border m-b-base"),b(O,"class","section-title"),b(q,"class","tabs-header compact left"),b(H,"class","tabs-content"),b(R,"class","tabs")},m(e,t){d(e,l,t),o(l,s),o(l,m),o(l,_),d(e,i,t),d(e,f,t),d(e,v,t),ne(g,e,t),d(e,w,t),d(e,B,t),d(e,J,t),d(e,$,t),o($,F),o($,ce),o($,L),o(L,M),o(M,de),o(M,K),o(K,Q),o(M,ue),o($,pe),o($,V),d(e,I,t),d(e,D,t),d(e,W,t),d(e,T,t),o(T,G),o(T,fe),o(T,X),o(X,C),o(C,Y),o(C,he),o(C,Z),o(C,be),o(C,h),o(h,me),ne(P,h,null),o(h,_e),o(h,ke),o(h,ve),o(h,ee),o(h,ge),o(h,te),o(h,ye),o(h,Se),o(h,$e),o(h,oe),o(h,we),d(e,le,t),d(e,O,t),d(e,se,t),d(e,R,t),o(R,q);for(let c=0;c<S.length;c+=1)S[c]&&S[c].m(q,null);o(R,Ce),o(R,H);for(let c=0;c<y.length;c+=1)y[c]&&y[c].m(H,null);A=!0},p(e,[t]){var De,Oe;(!A||t&1)&&n!==(n=e[0].name+"")&&re(m,n);const c={};t&9&&(c.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${e[3]}');
|
||||
@@ -48,7 +48,7 @@ import{S as ze,i as Ue,s as je,N as Ve,e as a,w as k,b as p,c as ae,f as b,g as
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
`),t&9&&(u.dart=`
|
||||
`),t&9&&(c.dart=`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${e[3]}');
|
||||
@@ -61,7 +61,7 @@ import{S as ze,i as Ue,s as je,N as Ve,e as a,w as k,b as p,c as ae,f as b,g as
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.model.id);
|
||||
`),g.$set(u),(!A||t&1)&&N!==(N=e[0].name+"")&&re(Q,N),t&6&&(z=e[2],$=qe($,t,Pe,1,e,z,Te,q,xe,Le,null,Fe)),t&6&&(E=e[2],Je(),y=qe(y,t,Ae,1,e,E,Re,H,Ke,Ne,null,Ee),Qe())},i(e){if(!A){U(g.$$.fragment,e),U(P.$$.fragment,e);for(let t=0;t<E.length;t+=1)U(y[t]);A=!0}},o(e){j(g.$$.fragment,e),j(P.$$.fragment,e);for(let t=0;t<y.length;t+=1)j(y[t]);A=!1},d(e){e&&d(l),e&&d(i),e&&d(f),e&&d(v),ie(g,e),e&&d(w),e&&d(B),e&&d(J),e&&d(S),e&&d(I),e&&d(D),e&&d(W),e&&d(T),ie(P),e&&d(le),e&&d(O),e&&d(se),e&&d(R);for(let t=0;t<$.length;t+=1)$[t].d();for(let t=0;t<y.length;t+=1)y[t].d()}}}function Ze(r,l,s){let n,{collection:m=new Ie}=l,_=200,i=[];const f=v=>s(1,_=v.code);return r.$$set=v=>{"collection"in v&&s(0,m=v.collection)},r.$$.update=()=>{r.$$.dirty&1&&s(2,i=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:He.dummyCollectionRecord(m)},null,2)},{code:401,body:`
|
||||
`),g.$set(c),(!A||t&1)&&N!==(N=e[0].name+"")&&re(Q,N),t&6&&(z=e[2],S=qe(S,t,Pe,1,e,z,Te,q,xe,Le,null,Fe)),t&6&&(E=e[2],Je(),y=qe(y,t,Ae,1,e,E,Re,H,Ke,Ne,null,Ee),Qe())},i(e){if(!A){U(g.$$.fragment,e),U(P.$$.fragment,e);for(let t=0;t<E.length;t+=1)U(y[t]);A=!0}},o(e){j(g.$$.fragment,e),j(P.$$.fragment,e);for(let t=0;t<y.length;t+=1)j(y[t]);A=!1},d(e){e&&u(l),e&&u(i),e&&u(f),e&&u(v),ie(g,e),e&&u(w),e&&u(B),e&&u(J),e&&u($),e&&u(I),e&&u(D),e&&u(W),e&&u(T),ie(P),e&&u(le),e&&u(O),e&&u(se),e&&u(R);for(let t=0;t<S.length;t+=1)S[t].d();for(let t=0;t<y.length;t+=1)y[t].d()}}}function Ze(r,l,s){let n,{collection:m=new Ie}=l,_=200,i=[];const f=v=>s(1,_=v.code);return r.$$set=v=>{"collection"in v&&s(0,m=v.collection)},r.$$.update=()=>{r.$$.dirty&1&&s(2,i=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:He.dummyCollectionRecord(m)},null,2)},{code:401,body:`
|
||||
{
|
||||
"code": 401,
|
||||
"message": "The request requires valid record authorization token to be set.",
|
||||
Vendored
+2
-2
@@ -1,4 +1,4 @@
|
||||
import{S as je,i as He,s as Je,N as We,e as s,w as v,b as p,c as re,f as h,g as r,h as a,m as ce,x as de,O as Ve,P as Ne,k as Qe,Q as ze,n as Ke,t as j,a as H,o as c,d as ue,R as Ye,C as Be,p as Ge,r as J,u as Xe}from"./index-4bd199fb.js";import{S as Ze}from"./SdkTabs-fc1a80c5.js";function Fe(i,l,o){const n=i.slice();return n[5]=l[o],n}function Le(i,l,o){const n=i.slice();return n[5]=l[o],n}function xe(i,l){let o,n=l[5].code+"",m,_,d,b;function g(){return l[4](l[5])}return{key:i,first:null,c(){o=s("button"),m=v(n),_=p(),h(o,"class","tab-item"),J(o,"active",l[1]===l[5].code),this.first=o},m(k,R){r(k,o,R),a(o,m),a(o,_),d||(b=Xe(o,"click",g),d=!0)},p(k,R){l=k,R&4&&n!==(n=l[5].code+"")&&de(m,n),R&6&&J(o,"active",l[1]===l[5].code)},d(k){k&&c(o),d=!1,b()}}}function Me(i,l){let o,n,m,_;return n=new We({props:{content:l[5].body}}),{key:i,first:null,c(){o=s("div"),re(n.$$.fragment),m=p(),h(o,"class","tab-item"),J(o,"active",l[1]===l[5].code),this.first=o},m(d,b){r(d,o,b),ce(n,o,null),a(o,m),_=!0},p(d,b){l=d;const g={};b&4&&(g.content=l[5].body),n.$set(g),(!_||b&6)&&J(o,"active",l[1]===l[5].code)},i(d){_||(j(n.$$.fragment,d),_=!0)},o(d){H(n.$$.fragment,d),_=!1},d(d){d&&c(o),ue(n)}}}function et(i){var qe,Ie;let l,o,n=i[0].name+"",m,_,d,b,g,k,R,C,N,y,L,pe,x,D,he,Q,M=i[0].name+"",z,be,K,q,Y,I,G,P,X,O,Z,fe,ee,$,te,me,ae,_e,f,ve,E,ge,ke,we,le,Se,oe,Re,ye,Oe,se,$e,ne,U,ie,A,V,S=[],Ae=new Map,Ee,B,w=[],Te=new Map,T;k=new Ze({props:{js:`
|
||||
import{S as je,i as He,s as Je,N as We,e as s,w as v,b as p,c as re,f as h,g as r,h as a,m as ce,x as de,O as Ve,P as Ne,k as Qe,Q as ze,n as Ke,t as j,a as H,o as c,d as ue,R as Ye,C as Be,p as Ge,r as J,u as Xe}from"./index-ea3aeb4c.js";import{S as Ze}from"./SdkTabs-27c932bb.js";function Fe(i,l,o){const n=i.slice();return n[5]=l[o],n}function Le(i,l,o){const n=i.slice();return n[5]=l[o],n}function xe(i,l){let o,n=l[5].code+"",m,_,d,b;function g(){return l[4](l[5])}return{key:i,first:null,c(){o=s("button"),m=v(n),_=p(),h(o,"class","tab-item"),J(o,"active",l[1]===l[5].code),this.first=o},m(k,R){r(k,o,R),a(o,m),a(o,_),d||(b=Xe(o,"click",g),d=!0)},p(k,R){l=k,R&4&&n!==(n=l[5].code+"")&&de(m,n),R&6&&J(o,"active",l[1]===l[5].code)},d(k){k&&c(o),d=!1,b()}}}function Me(i,l){let o,n,m,_;return n=new We({props:{content:l[5].body}}),{key:i,first:null,c(){o=s("div"),re(n.$$.fragment),m=p(),h(o,"class","tab-item"),J(o,"active",l[1]===l[5].code),this.first=o},m(d,b){r(d,o,b),ce(n,o,null),a(o,m),_=!0},p(d,b){l=d;const g={};b&4&&(g.content=l[5].body),n.$set(g),(!_||b&6)&&J(o,"active",l[1]===l[5].code)},i(d){_||(j(n.$$.fragment,d),_=!0)},o(d){H(n.$$.fragment,d),_=!1},d(d){d&&c(o),ue(n)}}}function et(i){var qe,Ie;let l,o,n=i[0].name+"",m,_,d,b,g,k,R,C,N,y,L,pe,x,D,he,Q,M=i[0].name+"",z,be,K,q,Y,I,G,P,X,O,Z,fe,ee,$,te,me,ae,_e,f,ve,E,ge,ke,we,le,Se,oe,Re,ye,Oe,se,$e,ne,U,ie,A,V,S=[],Ae=new Map,Ee,B,w=[],Te=new Map,T;k=new Ze({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${i[3]}');
|
||||
@@ -87,7 +87,7 @@ import{S as je,i as He,s as Je,N as We,e as s,w as v,b as p,c as re,f as h,g as
|
||||
The expanded relations will be appended to the record under the
|
||||
`),le=s("code"),le.textContent="expand",Se=v(" property (eg. "),oe=s("code"),oe.textContent='"expand": {"relField1": {...}, ...}',Re=v(`).
|
||||
`),ye=s("br"),Oe=v(`
|
||||
Only the relations to which the request user has permissions to `),se=s("strong"),se.textContent="view",$e=v(" will be expanded."),ne=p(),U=s("div"),U.textContent="Responses",ie=p(),A=s("div"),V=s("div");for(let e=0;e<S.length;e+=1)S[e].c();Ee=p(),B=s("div");for(let e=0;e<w.length;e+=1)w[e].c();h(l,"class","m-b-sm"),h(b,"class","content txt-lg m-b-sm"),h(C,"class","m-b-xs"),h(L,"class","label label-primary"),h(x,"class","content"),h(y,"class","alert alert-success"),h(q,"class","section-title"),h(I,"class","table-compact table-border m-b-base"),h(P,"class","section-title"),h(O,"class","table-compact table-border m-b-base"),h(U,"class","section-title"),h(V,"class","tabs-header compact left"),h(B,"class","tabs-content"),h(A,"class","tabs")},m(e,t){r(e,l,t),a(l,o),a(l,m),a(l,_),r(e,d,t),r(e,b,t),r(e,g,t),ce(k,e,t),r(e,R,t),r(e,C,t),r(e,N,t),r(e,y,t),a(y,L),a(y,pe),a(y,x),a(x,D),a(D,he),a(D,Q),a(Q,z),a(D,be),r(e,K,t),r(e,q,t),r(e,Y,t),r(e,I,t),r(e,G,t),r(e,P,t),r(e,X,t),r(e,O,t),a(O,Z),a(O,fe),a(O,ee),a(ee,$),a($,te),a($,me),a($,ae),a($,_e),a($,f),a(f,ve),ce(E,f,null),a(f,ge),a(f,ke),a(f,we),a(f,le),a(f,Se),a(f,oe),a(f,Re),a(f,ye),a(f,Oe),a(f,se),a(f,$e),r(e,ne,t),r(e,U,t),r(e,ie,t),r(e,A,t),a(A,V);for(let u=0;u<S.length;u+=1)S[u].m(V,null);a(A,Ee),a(A,B);for(let u=0;u<w.length;u+=1)w[u].m(B,null);T=!0},p(e,[t]){var Pe,Ue;(!T||t&1)&&n!==(n=e[0].name+"")&&de(m,n);const u={};t&9&&(u.js=`
|
||||
Only the relations to which the request user has permissions to `),se=s("strong"),se.textContent="view",$e=v(" will be expanded."),ne=p(),U=s("div"),U.textContent="Responses",ie=p(),A=s("div"),V=s("div");for(let e=0;e<S.length;e+=1)S[e].c();Ee=p(),B=s("div");for(let e=0;e<w.length;e+=1)w[e].c();h(l,"class","m-b-sm"),h(b,"class","content txt-lg m-b-sm"),h(C,"class","m-b-xs"),h(L,"class","label label-primary"),h(x,"class","content"),h(y,"class","alert alert-success"),h(q,"class","section-title"),h(I,"class","table-compact table-border m-b-base"),h(P,"class","section-title"),h(O,"class","table-compact table-border m-b-base"),h(U,"class","section-title"),h(V,"class","tabs-header compact left"),h(B,"class","tabs-content"),h(A,"class","tabs")},m(e,t){r(e,l,t),a(l,o),a(l,m),a(l,_),r(e,d,t),r(e,b,t),r(e,g,t),ce(k,e,t),r(e,R,t),r(e,C,t),r(e,N,t),r(e,y,t),a(y,L),a(y,pe),a(y,x),a(x,D),a(D,he),a(D,Q),a(Q,z),a(D,be),r(e,K,t),r(e,q,t),r(e,Y,t),r(e,I,t),r(e,G,t),r(e,P,t),r(e,X,t),r(e,O,t),a(O,Z),a(O,fe),a(O,ee),a(ee,$),a($,te),a($,me),a($,ae),a($,_e),a($,f),a(f,ve),ce(E,f,null),a(f,ge),a(f,ke),a(f,we),a(f,le),a(f,Se),a(f,oe),a(f,Re),a(f,ye),a(f,Oe),a(f,se),a(f,$e),r(e,ne,t),r(e,U,t),r(e,ie,t),r(e,A,t),a(A,V);for(let u=0;u<S.length;u+=1)S[u]&&S[u].m(V,null);a(A,Ee),a(A,B);for(let u=0;u<w.length;u+=1)w[u]&&w[u].m(B,null);T=!0},p(e,[t]){var Pe,Ue;(!T||t&1)&&n!==(n=e[0].name+"")&&de(m,n);const u={};t&9&&(u.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${e[3]}');
|
||||
Vendored
+6
-6
@@ -1,4 +1,4 @@
|
||||
import{S as Se,i as ve,s as we,N as ke,e as s,w as f,b as u,c as Ot,f as h,g as r,h as o,m as At,x as Tt,O as ce,P as ye,k as ge,Q as Pe,n as Re,t as tt,a as et,o as c,d as Ut,R as $e,C as de,p as Ce,r as lt,u as Oe}from"./index-4bd199fb.js";import{S as Ae}from"./SdkTabs-fc1a80c5.js";function ue(n,e,l){const i=n.slice();return i[8]=e[l],i}function fe(n,e,l){const i=n.slice();return i[8]=e[l],i}function Te(n){let e;return{c(){e=f("email")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function Ue(n){let e;return{c(){e=f("username")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function Me(n){let e;return{c(){e=f("username/email")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function pe(n){let e;return{c(){e=s("strong"),e.textContent="username"},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function be(n){let e;return{c(){e=f("or")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function me(n){let e;return{c(){e=s("strong"),e.textContent="email"},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function he(n,e){let l,i=e[8].code+"",S,m,p,d;function _(){return e[7](e[8])}return{key:n,first:null,c(){l=s("button"),S=f(i),m=u(),h(l,"class","tab-item"),lt(l,"active",e[3]===e[8].code),this.first=l},m($,C){r($,l,C),o(l,S),o(l,m),p||(d=Oe(l,"click",_),p=!0)},p($,C){e=$,C&16&&i!==(i=e[8].code+"")&&Tt(S,i),C&24&<(l,"active",e[3]===e[8].code)},d($){$&&c(l),p=!1,d()}}}function _e(n,e){let l,i,S,m;return i=new ke({props:{content:e[8].body}}),{key:n,first:null,c(){l=s("div"),Ot(i.$$.fragment),S=u(),h(l,"class","tab-item"),lt(l,"active",e[3]===e[8].code),this.first=l},m(p,d){r(p,l,d),At(i,l,null),o(l,S),m=!0},p(p,d){e=p;const _={};d&16&&(_.content=e[8].body),i.$set(_),(!m||d&24)&<(l,"active",e[3]===e[8].code)},i(p){m||(tt(i.$$.fragment,p),m=!0)},o(p){et(i.$$.fragment,p),m=!1},d(p){p&&c(l),Ut(i)}}}function De(n){var se,ne;let e,l,i=n[0].name+"",S,m,p,d,_,$,C,O,B,Mt,ot,T,at,F,st,U,G,Dt,X,N,Et,nt,Z=n[0].name+"",it,Wt,rt,I,ct,M,dt,Lt,V,D,ut,Bt,ft,Ht,g,Yt,pt,bt,mt,qt,ht,_t,j,kt,E,St,Ft,vt,W,wt,Nt,yt,It,k,Vt,H,jt,Jt,Qt,gt,Kt,Pt,zt,Gt,Xt,Rt,Zt,$t,J,Ct,L,Q,A=[],xt=new Map,te,K,P=[],ee=new Map,Y;function le(t,a){if(t[1]&&t[2])return Me;if(t[1])return Ue;if(t[2])return Te}let q=le(n),R=q&&q(n);T=new Ae({props:{js:`
|
||||
import{S as Se,i as ve,s as we,N as ke,e as s,w as f,b as u,c as Ot,f as h,g as r,h as o,m as At,x as Tt,O as ce,P as ye,k as ge,Q as Pe,n as Re,t as tt,a as et,o as c,d as Ut,R as $e,C as de,p as Ce,r as lt,u as Oe}from"./index-ea3aeb4c.js";import{S as Ae}from"./SdkTabs-27c932bb.js";function ue(n,e,l){const i=n.slice();return i[8]=e[l],i}function fe(n,e,l){const i=n.slice();return i[8]=e[l],i}function Te(n){let e;return{c(){e=f("email")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function Ue(n){let e;return{c(){e=f("username")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function Me(n){let e;return{c(){e=f("username/email")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function pe(n){let e;return{c(){e=s("strong"),e.textContent="username"},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function be(n){let e;return{c(){e=f("or")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function me(n){let e;return{c(){e=s("strong"),e.textContent="email"},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function he(n,e){let l,i=e[8].code+"",S,m,p,d;function _(){return e[7](e[8])}return{key:n,first:null,c(){l=s("button"),S=f(i),m=u(),h(l,"class","tab-item"),lt(l,"active",e[3]===e[8].code),this.first=l},m($,C){r($,l,C),o(l,S),o(l,m),p||(d=Oe(l,"click",_),p=!0)},p($,C){e=$,C&16&&i!==(i=e[8].code+"")&&Tt(S,i),C&24&<(l,"active",e[3]===e[8].code)},d($){$&&c(l),p=!1,d()}}}function _e(n,e){let l,i,S,m;return i=new ke({props:{content:e[8].body}}),{key:n,first:null,c(){l=s("div"),Ot(i.$$.fragment),S=u(),h(l,"class","tab-item"),lt(l,"active",e[3]===e[8].code),this.first=l},m(p,d){r(p,l,d),At(i,l,null),o(l,S),m=!0},p(p,d){e=p;const _={};d&16&&(_.content=e[8].body),i.$set(_),(!m||d&24)&<(l,"active",e[3]===e[8].code)},i(p){m||(tt(i.$$.fragment,p),m=!0)},o(p){et(i.$$.fragment,p),m=!1},d(p){p&&c(l),Ut(i)}}}function De(n){var se,ne;let e,l,i=n[0].name+"",S,m,p,d,_,$,C,O,B,Mt,ot,T,at,F,st,U,G,Dt,X,N,Et,nt,Z=n[0].name+"",it,Wt,rt,I,ct,M,dt,Lt,V,D,ut,Bt,ft,Ht,P,Yt,pt,bt,mt,qt,ht,_t,j,kt,E,St,Ft,vt,W,wt,Nt,yt,It,k,Vt,H,jt,Jt,Qt,gt,Kt,Pt,zt,Gt,Xt,Rt,Zt,$t,J,Ct,L,Q,A=[],xt=new Map,te,K,v=[],ee=new Map,Y;function le(t,a){if(t[1]&&t[2])return Me;if(t[1])return Ue;if(t[2])return Te}let q=le(n),R=q&&q(n);T=new Ae({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${n[6]}');
|
||||
@@ -36,13 +36,13 @@ import{S as Se,i as ve,s as we,N as ke,e as s,w as f,b as u,c as Ot,f as h,g as
|
||||
|
||||
// "logout" the last authenticated account
|
||||
pb.authStore.clear();
|
||||
`}});let v=n[1]&&pe(),w=n[1]&&n[2]&&be(),y=n[2]&&me();H=new ke({props:{content:"?expand=relField1,relField2.subRelField"}});let x=n[4];const oe=t=>t[8].code;for(let t=0;t<x.length;t+=1){let a=fe(n,x,t),b=oe(a);xt.set(b,A[t]=he(b,a))}let z=n[4];const ae=t=>t[8].code;for(let t=0;t<z.length;t+=1){let a=ue(n,z,t),b=ae(a);ee.set(b,P[t]=_e(b,a))}return{c(){e=s("h3"),l=f("Auth with password ("),S=f(i),m=f(")"),p=u(),d=s("div"),_=s("p"),$=f(`Returns new auth token and account data by a combination of
|
||||
`}});let w=n[1]&&pe(),y=n[1]&&n[2]&&be(),g=n[2]&&me();H=new ke({props:{content:"?expand=relField1,relField2.subRelField"}});let x=n[4];const oe=t=>t[8].code;for(let t=0;t<x.length;t+=1){let a=fe(n,x,t),b=oe(a);xt.set(b,A[t]=he(b,a))}let z=n[4];const ae=t=>t[8].code;for(let t=0;t<z.length;t+=1){let a=ue(n,z,t),b=ae(a);ee.set(b,v[t]=_e(b,a))}return{c(){e=s("h3"),l=f("Auth with password ("),S=f(i),m=f(")"),p=u(),d=s("div"),_=s("p"),$=f(`Returns new auth token and account data by a combination of
|
||||
`),C=s("strong"),R&&R.c(),O=f(`
|
||||
and `),B=s("strong"),B.textContent="password",Mt=f("."),ot=u(),Ot(T.$$.fragment),at=u(),F=s("h6"),F.textContent="API details",st=u(),U=s("div"),G=s("strong"),G.textContent="POST",Dt=u(),X=s("div"),N=s("p"),Et=f("/api/collections/"),nt=s("strong"),it=f(Z),Wt=f("/auth-with-password"),rt=u(),I=s("div"),I.textContent="Body Parameters",ct=u(),M=s("table"),dt=s("thead"),dt.innerHTML=`<tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th></tr>`,Lt=u(),V=s("tbody"),D=s("tr"),ut=s("td"),ut.innerHTML=`<div class="inline-flex"><span class="label label-success">Required</span>
|
||||
<span>identity</span></div>`,Bt=u(),ft=s("td"),ft.innerHTML='<span class="label">String</span>',Ht=u(),g=s("td"),Yt=f(`The
|
||||
`),v&&v.c(),pt=u(),w&&w.c(),bt=u(),y&&y.c(),mt=f(`
|
||||
<span>identity</span></div>`,Bt=u(),ft=s("td"),ft.innerHTML='<span class="label">String</span>',Ht=u(),P=s("td"),Yt=f(`The
|
||||
`),w&&w.c(),pt=u(),y&&y.c(),bt=u(),g&&g.c(),mt=f(`
|
||||
of the record to authenticate.`),qt=u(),ht=s("tr"),ht.innerHTML=`<td><div class="inline-flex"><span class="label label-success">Required</span>
|
||||
<span>password</span></div></td>
|
||||
<td><span class="label">String</span></td>
|
||||
@@ -54,7 +54,7 @@ import{S as Se,i as ve,s as we,N as ke,e as s,w as f,b as u,c as Ot,f as h,g as
|
||||
The expanded relations will be appended to the record under the
|
||||
`),gt=s("code"),gt.textContent="expand",Kt=f(" property (eg. "),Pt=s("code"),Pt.textContent='"expand": {"relField1": {...}, ...}',zt=f(`).
|
||||
`),Gt=s("br"),Xt=f(`
|
||||
Only the relations to which the request user has permissions to `),Rt=s("strong"),Rt.textContent="view",Zt=f(" will be expanded."),$t=u(),J=s("div"),J.textContent="Responses",Ct=u(),L=s("div"),Q=s("div");for(let t=0;t<A.length;t+=1)A[t].c();te=u(),K=s("div");for(let t=0;t<P.length;t+=1)P[t].c();h(e,"class","m-b-sm"),h(d,"class","content txt-lg m-b-sm"),h(F,"class","m-b-xs"),h(G,"class","label label-primary"),h(X,"class","content"),h(U,"class","alert alert-success"),h(I,"class","section-title"),h(M,"class","table-compact table-border m-b-base"),h(j,"class","section-title"),h(E,"class","table-compact table-border m-b-base"),h(J,"class","section-title"),h(Q,"class","tabs-header compact left"),h(K,"class","tabs-content"),h(L,"class","tabs")},m(t,a){r(t,e,a),o(e,l),o(e,S),o(e,m),r(t,p,a),r(t,d,a),o(d,_),o(_,$),o(_,C),R&&R.m(C,null),o(_,O),o(_,B),o(_,Mt),r(t,ot,a),At(T,t,a),r(t,at,a),r(t,F,a),r(t,st,a),r(t,U,a),o(U,G),o(U,Dt),o(U,X),o(X,N),o(N,Et),o(N,nt),o(nt,it),o(N,Wt),r(t,rt,a),r(t,I,a),r(t,ct,a),r(t,M,a),o(M,dt),o(M,Lt),o(M,V),o(V,D),o(D,ut),o(D,Bt),o(D,ft),o(D,Ht),o(D,g),o(g,Yt),v&&v.m(g,null),o(g,pt),w&&w.m(g,null),o(g,bt),y&&y.m(g,null),o(g,mt),o(V,qt),o(V,ht),r(t,_t,a),r(t,j,a),r(t,kt,a),r(t,E,a),o(E,St),o(E,Ft),o(E,vt),o(vt,W),o(W,wt),o(W,Nt),o(W,yt),o(W,It),o(W,k),o(k,Vt),At(H,k,null),o(k,jt),o(k,Jt),o(k,Qt),o(k,gt),o(k,Kt),o(k,Pt),o(k,zt),o(k,Gt),o(k,Xt),o(k,Rt),o(k,Zt),r(t,$t,a),r(t,J,a),r(t,Ct,a),r(t,L,a),o(L,Q);for(let b=0;b<A.length;b+=1)A[b].m(Q,null);o(L,te),o(L,K);for(let b=0;b<P.length;b+=1)P[b].m(K,null);Y=!0},p(t,[a]){var ie,re;(!Y||a&1)&&i!==(i=t[0].name+"")&&Tt(S,i),q!==(q=le(t))&&(R&&R.d(1),R=q&&q(t),R&&(R.c(),R.m(C,null)));const b={};a&97&&(b.js=`
|
||||
Only the relations to which the request user has permissions to `),Rt=s("strong"),Rt.textContent="view",Zt=f(" will be expanded."),$t=u(),J=s("div"),J.textContent="Responses",Ct=u(),L=s("div"),Q=s("div");for(let t=0;t<A.length;t+=1)A[t].c();te=u(),K=s("div");for(let t=0;t<v.length;t+=1)v[t].c();h(e,"class","m-b-sm"),h(d,"class","content txt-lg m-b-sm"),h(F,"class","m-b-xs"),h(G,"class","label label-primary"),h(X,"class","content"),h(U,"class","alert alert-success"),h(I,"class","section-title"),h(M,"class","table-compact table-border m-b-base"),h(j,"class","section-title"),h(E,"class","table-compact table-border m-b-base"),h(J,"class","section-title"),h(Q,"class","tabs-header compact left"),h(K,"class","tabs-content"),h(L,"class","tabs")},m(t,a){r(t,e,a),o(e,l),o(e,S),o(e,m),r(t,p,a),r(t,d,a),o(d,_),o(_,$),o(_,C),R&&R.m(C,null),o(_,O),o(_,B),o(_,Mt),r(t,ot,a),At(T,t,a),r(t,at,a),r(t,F,a),r(t,st,a),r(t,U,a),o(U,G),o(U,Dt),o(U,X),o(X,N),o(N,Et),o(N,nt),o(nt,it),o(N,Wt),r(t,rt,a),r(t,I,a),r(t,ct,a),r(t,M,a),o(M,dt),o(M,Lt),o(M,V),o(V,D),o(D,ut),o(D,Bt),o(D,ft),o(D,Ht),o(D,P),o(P,Yt),w&&w.m(P,null),o(P,pt),y&&y.m(P,null),o(P,bt),g&&g.m(P,null),o(P,mt),o(V,qt),o(V,ht),r(t,_t,a),r(t,j,a),r(t,kt,a),r(t,E,a),o(E,St),o(E,Ft),o(E,vt),o(vt,W),o(W,wt),o(W,Nt),o(W,yt),o(W,It),o(W,k),o(k,Vt),At(H,k,null),o(k,jt),o(k,Jt),o(k,Qt),o(k,gt),o(k,Kt),o(k,Pt),o(k,zt),o(k,Gt),o(k,Xt),o(k,Rt),o(k,Zt),r(t,$t,a),r(t,J,a),r(t,Ct,a),r(t,L,a),o(L,Q);for(let b=0;b<A.length;b+=1)A[b]&&A[b].m(Q,null);o(L,te),o(L,K);for(let b=0;b<v.length;b+=1)v[b]&&v[b].m(K,null);Y=!0},p(t,[a]){var ie,re;(!Y||a&1)&&i!==(i=t[0].name+"")&&Tt(S,i),q!==(q=le(t))&&(R&&R.d(1),R=q&&q(t),R&&(R.c(),R.m(C,null)));const b={};a&97&&(b.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${t[6]}');
|
||||
@@ -92,7 +92,7 @@ import{S as Se,i as ve,s as we,N as ke,e as s,w as f,b as u,c as Ot,f as h,g as
|
||||
|
||||
// "logout" the last authenticated account
|
||||
pb.authStore.clear();
|
||||
`),T.$set(b),(!Y||a&1)&&Z!==(Z=t[0].name+"")&&Tt(it,Z),t[1]?v||(v=pe(),v.c(),v.m(g,pt)):v&&(v.d(1),v=null),t[1]&&t[2]?w||(w=be(),w.c(),w.m(g,bt)):w&&(w.d(1),w=null),t[2]?y||(y=me(),y.c(),y.m(g,mt)):y&&(y.d(1),y=null),a&24&&(x=t[4],A=ce(A,a,oe,1,t,x,xt,Q,ye,he,null,fe)),a&24&&(z=t[4],ge(),P=ce(P,a,ae,1,t,z,ee,K,Pe,_e,null,ue),Re())},i(t){if(!Y){tt(T.$$.fragment,t),tt(H.$$.fragment,t);for(let a=0;a<z.length;a+=1)tt(P[a]);Y=!0}},o(t){et(T.$$.fragment,t),et(H.$$.fragment,t);for(let a=0;a<P.length;a+=1)et(P[a]);Y=!1},d(t){t&&c(e),t&&c(p),t&&c(d),R&&R.d(),t&&c(ot),Ut(T,t),t&&c(at),t&&c(F),t&&c(st),t&&c(U),t&&c(rt),t&&c(I),t&&c(ct),t&&c(M),v&&v.d(),w&&w.d(),y&&y.d(),t&&c(_t),t&&c(j),t&&c(kt),t&&c(E),Ut(H),t&&c($t),t&&c(J),t&&c(Ct),t&&c(L);for(let a=0;a<A.length;a+=1)A[a].d();for(let a=0;a<P.length;a+=1)P[a].d()}}}function Ee(n,e,l){let i,S,m,p,{collection:d=new $e}=e,_=200,$=[];const C=O=>l(3,_=O.code);return n.$$set=O=>{"collection"in O&&l(0,d=O.collection)},n.$$.update=()=>{var O,B;n.$$.dirty&1&&l(2,S=(O=d==null?void 0:d.options)==null?void 0:O.allowEmailAuth),n.$$.dirty&1&&l(1,m=(B=d==null?void 0:d.options)==null?void 0:B.allowUsernameAuth),n.$$.dirty&6&&l(5,p=m&&S?"YOUR_USERNAME_OR_EMAIL":m?"YOUR_USERNAME":"YOUR_EMAIL"),n.$$.dirty&1&&l(4,$=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:de.dummyCollectionRecord(d)},null,2)},{code:400,body:`
|
||||
`),T.$set(b),(!Y||a&1)&&Z!==(Z=t[0].name+"")&&Tt(it,Z),t[1]?w||(w=pe(),w.c(),w.m(P,pt)):w&&(w.d(1),w=null),t[1]&&t[2]?y||(y=be(),y.c(),y.m(P,bt)):y&&(y.d(1),y=null),t[2]?g||(g=me(),g.c(),g.m(P,mt)):g&&(g.d(1),g=null),a&24&&(x=t[4],A=ce(A,a,oe,1,t,x,xt,Q,ye,he,null,fe)),a&24&&(z=t[4],ge(),v=ce(v,a,ae,1,t,z,ee,K,Pe,_e,null,ue),Re())},i(t){if(!Y){tt(T.$$.fragment,t),tt(H.$$.fragment,t);for(let a=0;a<z.length;a+=1)tt(v[a]);Y=!0}},o(t){et(T.$$.fragment,t),et(H.$$.fragment,t);for(let a=0;a<v.length;a+=1)et(v[a]);Y=!1},d(t){t&&c(e),t&&c(p),t&&c(d),R&&R.d(),t&&c(ot),Ut(T,t),t&&c(at),t&&c(F),t&&c(st),t&&c(U),t&&c(rt),t&&c(I),t&&c(ct),t&&c(M),w&&w.d(),y&&y.d(),g&&g.d(),t&&c(_t),t&&c(j),t&&c(kt),t&&c(E),Ut(H),t&&c($t),t&&c(J),t&&c(Ct),t&&c(L);for(let a=0;a<A.length;a+=1)A[a].d();for(let a=0;a<v.length;a+=1)v[a].d()}}}function Ee(n,e,l){let i,S,m,p,{collection:d=new $e}=e,_=200,$=[];const C=O=>l(3,_=O.code);return n.$$set=O=>{"collection"in O&&l(0,d=O.collection)},n.$$.update=()=>{var O,B;n.$$.dirty&1&&l(2,S=(O=d==null?void 0:d.options)==null?void 0:O.allowEmailAuth),n.$$.dirty&1&&l(1,m=(B=d==null?void 0:d.options)==null?void 0:B.allowUsernameAuth),n.$$.dirty&6&&l(5,p=m&&S?"YOUR_USERNAME_OR_EMAIL":m?"YOUR_USERNAME":"YOUR_EMAIL"),n.$$.dirty&1&&l(4,$=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:de.dummyCollectionRecord(d)},null,2)},{code:400,body:`
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to authenticate.",
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import{S as ut,i as St,s as ft,e as dt,f as $t,T as I,g as gt,y as jO,o as Pt,I as mt,J as bt,K as Xt,L as Zt,C as xt,M as yt}from"./index-4bd199fb.js";import{P as kt,N as vt,u as Yt,D as wt,v as vO,T as B,I as xe,w as rO,x as l,y as Tt,L as iO,z as sO,A as R,B as nO,F as ye,G as lO,H as _,J as ke,K as ve,E as w,M as z,O as Wt,Q as Vt,R as Ye,U as b,V as Ut,W as _t,X as Ct,a as C,h as qt,b as jt,c as Gt,d as Rt,e as zt,s as At,f as It,g as Et,i as Nt,r as Bt,j as Dt,k as Lt,l as Jt,m as Mt,n as Ht,o as Ft,p as Kt,q as Oa,t as GO,C as E}from"./index-a6ccb683.js";class M{constructor(O,a,t,r,i,s,n,o,Q,p=0,c){this.p=O,this.stack=a,this.state=t,this.reducePos=r,this.pos=i,this.score=s,this.buffer=n,this.bufferBase=o,this.curContext=Q,this.lookAhead=p,this.parent=c}toString(){return`[${this.stack.filter((O,a)=>a%3==0).concat(this.state)}]@${this.pos}${this.score?"!"+this.score:""}`}static start(O,a,t=0){let r=O.parser.context;return new M(O,[],a,t,t,0,[],0,r?new RO(r,r.start):null,0,null)}get context(){return this.curContext?this.curContext.context:null}pushState(O,a){this.stack.push(this.state,a,this.bufferBase+this.buffer.length),this.state=O}reduce(O){var a;let t=O>>19,r=O&65535,{parser:i}=this.p,s=i.dynamicPrecedence(r);if(s&&(this.score+=s),t==0){this.pushState(i.getGoto(this.state,r,!0),this.reducePos),r<i.minRepeatTerm&&this.storeNode(r,this.reducePos,this.reducePos,4,!0),this.reduceContext(r,this.reducePos);return}let n=this.stack.length-(t-1)*3-(O&262144?6:0),o=n?this.stack[n-2]:this.p.ranges[0].from,Q=this.reducePos-o;Q>=2e3&&!(!((a=this.p.parser.nodeSet.types[r])===null||a===void 0)&&a.isAnonymous)&&(o==this.p.lastBigReductionStart?(this.p.bigReductionCount++,this.p.lastBigReductionSize=Q):this.p.lastBigReductionSize<Q&&(this.p.bigReductionCount=1,this.p.lastBigReductionStart=o,this.p.lastBigReductionSize=Q));let p=n?this.stack[n-1]:0,c=this.bufferBase+this.buffer.length-p;if(r<i.minRepeatTerm||O&131072){let u=i.stateFlag(this.state,1)?this.pos:this.reducePos;this.storeNode(r,o,u,c+4,!0)}if(O&262144)this.state=this.stack[n];else{let u=this.stack[n-3];this.state=i.getGoto(u,r,!0)}for(;this.stack.length>n;)this.stack.pop();this.reduceContext(r,o)}storeNode(O,a,t,r=4,i=!1){if(O==0&&(!this.stack.length||this.stack[this.stack.length-1]<this.buffer.length+this.bufferBase)){let s=this,n=this.buffer.length;if(n==0&&s.parent&&(n=s.bufferBase-s.parent.bufferBase,s=s.parent),n>0&&s.buffer[n-4]==0&&s.buffer[n-1]>-1){if(a==t)return;if(s.buffer[n-2]>=a){s.buffer[n-2]=t;return}}}if(!i||this.pos==t)this.buffer.push(O,a,t,r);else{let s=this.buffer.length;if(s>0&&this.buffer[s-4]!=0)for(;s>0&&this.buffer[s-2]>t;)this.buffer[s]=this.buffer[s-4],this.buffer[s+1]=this.buffer[s-3],this.buffer[s+2]=this.buffer[s-2],this.buffer[s+3]=this.buffer[s-1],s-=4,r>4&&(r-=4);this.buffer[s]=O,this.buffer[s+1]=a,this.buffer[s+2]=t,this.buffer[s+3]=r}}shift(O,a,t){let r=this.pos;if(O&131072)this.pushState(O&65535,this.pos);else if(O&262144)this.pos=t,this.shiftContext(a,r),a<=this.p.parser.maxNode&&this.buffer.push(a,r,t,4);else{let i=O,{parser:s}=this.p;(t>this.pos||a<=s.maxNode)&&(this.pos=t,s.stateFlag(i,1)||(this.reducePos=t)),this.pushState(i,r),this.shiftContext(a,r),a<=s.maxNode&&this.buffer.push(a,r,t,4)}}apply(O,a,t){O&65536?this.reduce(O):this.shift(O,a,t)}useNode(O,a){let t=this.p.reused.length-1;(t<0||this.p.reused[t]!=O)&&(this.p.reused.push(O),t++);let r=this.pos;this.reducePos=this.pos=r+O.length,this.pushState(a,r),this.buffer.push(t,r,this.reducePos,-1),this.curContext&&this.updateContext(this.curContext.tracker.reuse(this.curContext.context,O,this,this.p.stream.reset(this.pos-O.length)))}split(){let O=this,a=O.buffer.length;for(;a>0&&O.buffer[a-2]>O.reducePos;)a-=4;let t=O.buffer.slice(a),r=O.bufferBase+a;for(;O&&r==O.bufferBase;)O=O.parent;return new M(this.p,this.stack.slice(),this.state,this.reducePos,this.pos,this.score,t,r,this.curContext,this.lookAhead,O)}recoverByDelete(O,a){let t=O<=this.p.parser.maxNode;t&&this.storeNode(O,this.pos,a,4),this.storeNode(0,this.pos,a,t?8:4),this.pos=this.reducePos=a,this.score-=190}canShift(O){for(let a=new ea(this);;){let t=this.p.parser.stateSlot(a.state,4)||this.p.parser.hasAction(a.state,O);if(t==0)return!1;if(!(t&65536))return!0;a.reduce(t)}}recoverByInsert(O){if(this.stack.length>=300)return[];let a=this.p.parser.nextStates(this.state);if(a.length>4<<1||this.stack.length>=120){let r=[];for(let i=0,s;i<a.length;i+=2)(s=a[i+1])!=this.state&&this.p.parser.hasAction(s,O)&&r.push(a[i],s);if(this.stack.length<120)for(let i=0;r.length<4<<1&&i<a.length;i+=2){let s=a[i+1];r.some((n,o)=>o&1&&n==s)||r.push(a[i],s)}a=r}let t=[];for(let r=0;r<a.length&&t.length<4;r+=2){let i=a[r+1];if(i==this.state)continue;let s=this.split();s.pushState(i,this.pos),s.storeNode(0,s.pos,s.pos,4,!0),s.shiftContext(a[r],this.pos),s.score-=200,t.push(s)}return t}forceReduce(){let O=this.p.parser.stateSlot(this.state,5);if(!(O&65536))return!1;let{parser:a}=this.p;if(!a.validAction(this.state,O)){let t=O>>19,Line truncated
|
||||
\${}
|
||||
} catch (\${error}) {
|
||||
\${}
|
||||
}`,{label:"try",detail:"/ catch block",type:"keyword"}),b("if (${}) {\n ${}\n}",{label:"if",detail:"block",type:"keyword"}),b(`if (\${}) {
|
||||
\${}
|
||||
} else {
|
||||
\${}
|
||||
}`,{label:"if",detail:"/ else block",type:"keyword"}),b(`class \${name} {
|
||||
constructor(\${params}) {
|
||||
\${}
|
||||
}
|
||||
}`,{label:"class",detail:"definition",type:"keyword"}),b('import {${names}} from "${module}"\n${}',{label:"import",detail:"named",type:"keyword"}),b('import ${name} from "${module}"\n${}',{label:"import",detail:"default",type:"keyword"})],ce=new _t,Be=new Set(["Script","Block","FunctionExpression","FunctionDeclaration","ArrowFunction","MethodDeclaration","ForStatement"]);function q(e){return(O,a)=>{let t=O.node.getChild("VariableDefinition");return t&&a(t,e),!0}}const Vr=["FunctionDeclaration"],Ur={FunctionDeclaration:q("function"),ClassDeclaration:q("class"),ClassExpression:()=>!0,EnumDeclaration:q("constant"),TypeAliasDeclaration:q("type"),NamespaceDeclaration:q("namespace"),VariableDefinition(e,O){e.matchContext(Vr)||O(e,"variable")},TypeDefinition(e,O){O(e,"type")},__proto__:null};function De(e,O){let a=ce.get(O);if(a)return a;let t=[],r=!0;function i(s,n){let o=e.sliceString(s.from,s.to);t.push({label:o,type:n})}return O.cursor(xe.IncludeAnonymous).iterate(s=>{if(r)r=!1;else if(s.name){let n=Ur[s.name];if(n&&n(s,i)||Be.has(s.name))return!1}else if(s.to-s.from>8192){for(let n of De(e,s.node))t.push(n);return!1}}),ce.set(O,t),t}const he=/^[\w$\xa1-\uffff][\w$\d\xa1-\uffff]*$/,Le=["TemplateString","String","RegExp","LineComment","BlockComment","VariableDefinition","TypeDefinition","Label","PropertyDefinition","PropertyName","PrivatePropertyDefinition","PrivatePropertyName"];function _r(e){let O=_(e.state).resolveInner(e.pos,-1);if(Le.indexOf(O.name)>-1)return null;let a=O.name=="VariableName"||O.to-O.from<20&&he.test(e.state.sliceDoc(O.from,O.to));if(!a&&!e.explicit)return null;let t=[];for(let r=O;r;r=r.parent)Be.has(r.name)&&(t=t.concat(De(e.state.doc,r)));return{options:t,from:a?O.from:e.pos,validFor:he}}const y=iO.define({name:"javascript",parser:Tr.configure({props:[sO.add({IfStatement:R({except:/^\s*({|else\b)/}),TryStatement:R({except:/^\s*({|catch\b|finally\b)/}),LabeledStatement:Wt,SwitchBody:e=>{let O=e.textAfter,a=/^\s*\}/.test(O),t=/^\s*(case|default)\b/.test(O);return e.baseIndent+(a?0:t?1:2)*e.unit},Block:Vt({closing:"}"}),ArrowFunction:e=>e.baseIndent+e.unit,"TemplateString BlockComment":()=>null,"Statement Property":R({except:/^{/}),JSXElement(e){let O=/^\s*<\//.test(e.textAfter);return e.lineIndent(e.node.from)+(O?0:e.unit)},JSXEscape(e){let O=/\s*\}/.test(e.textAfter);return e.lineIndent(e.node.from)+(O?0:e.unit)},"JSXOpenTag JSXSelfClosingTag"(e){return e.column(e.node.from)+e.unit}}),nO.add({"Block ClassBody SwitchBody EnumBody ObjectExpression ArrayExpression":ye,BlockComment(e){return{from:e.from+2,to:e.to-2}}})]}),languageData:{closeBrackets:{brackets:["(","[","{","'",'"',"`"]},commentTokens:{line:"//",block:{open:"/*",close:"*/"}},indentOnInput:/^\s*(?:case |default:|\{|\}|<\/)$/,wordChars:"$"}}),Je={test:e=>/^JSX/.test(e.name),facet:Ut({commentTokens:{block:{open:"{/*",close:"*/}"}}})},Me=y.configure({dialect:"ts"},"typescript"),He=y.configure({dialect:"jsx",props:[Ye.add(e=>e.isTop?[Je]:void 0)]}),Fe=y.configure({dialect:"jsx ts",props:[Ye.add(e=>e.isTop?[Je]:void 0)]},"typescript"),Cr="break case const continue default delete export extends false finally in instanceof let new return static super switch this throw true typeof var yield".split(" ").map(e=>({label:e,type:"keyword"}));function Ke(e={}){let O=e.jsx?e.typescript?Fe:He:e.typescript?Me:y;return new lO(O,[y.data.of({autocomplete:ke(Le,ve(Wr.concat(Cr)))}),y.data.of({autocomplete:_r}),e.jsx?Gr:[]])}function qr(e){for(;;){if(e.name=="JSXOpenTag"||e.name=="JSXSelfClosingTag"||e.name=="JSXFragmentTag")return e;if(!e.parent)return null;e=e.parent}}function pe(e,O,a=e.length){for(let t=O==null?void 0:O.firstChild;t;t=t.nextSibling)if(t.name=="JSXIdentifier"||t.name=="JSXBuiltin"||t.name=="JSXNamespacedName"||t.name=="JSXMemberExpression")return e.sliceString(t.from,Math.min(t.to,a));return""}const jr=typeof navigator=="object"&&/Android\b/.test(navigator.userAgent),Gr=w.inputHandler.of((e,O,a,t)=>{if((jr?e.composing:e.compositionStarted)||e.state.readOnly||O!=a||t!=">"&&t!="/"||!y.isActiveAt(e.state,O,-1))return!1;let{state:r}=e,i=r.changeByRange(s=>{var n,o;let{head:Q}=s,p=_(r).resolveInner(Q,-1),c;if(p.name=="JSXStartTag"&&(p=p.parent),t==">"&&p.name=="JSXFragmentTag")return{range:z.cursor(Q+1),changes:{from:Q,insert:"></>"}};if(t=="/"&&p.name=="JSXFragmentTag"){let u=p.parent,h=u==null?void 0:u.parent;if(u.from==Q-1&&((n=h.lastChild)===null||n===void 0?void 0:n.name)!="JSXEndTag"&&(c=pe(r.doc,h==null?void 0:h.firstChild,Q))){let f=`/${c}>`;return{range:z.cursor(Q+f.length),changes:{from:Q,insert:f}}}}else if(t==">"){let u=qr(p);if(u&&((o=u.lastChild)===null||o===void 0?void 0:o.name)!="JSXEndTag"&&r.sliceDoc(Q,Q+2)!="</"&&(c=pe(r.doc,u,Q)))return{range:z.cursor(Q+1),changes:{from:Q,insert:`></${c}>`}}}return{range:s}});return i.changes.empty?!1:(e.dispatch(i,{userEvent:"input.type",scrollIntoView:!0}),!0)}),j=["_blank","_self","_top","_parent"],$O=["ascii","utf-8","utf-16","latin1","latin1"],gO=["get","post","put","delete"
Line truncated
|
||||
`;function ot(e,O,a){let t=Object.create(null);t.true=t.false=Mr,t.null=t.unknown=Hr;for(let r of e.split(" "))r&&(t[r]=st);for(let r of O.split(" "))r&&(t[r]=nt);for(let r of(a||"").split(" "))r&&(t[r]=ci);return t}const Si="array binary bit boolean char character clob date decimal double float int integer interval large national nchar nclob numeric object precision real smallint time timestamp varchar varying ",fi="absolute action add after all allocate alter and any are as asc assertion at authorization before begin between both breadth by call cascade cascaded case cast catalog check close collate collation column commit condition connect connection constraint constraints constructor continue corresponding count create cross cube current current_date current_default_transform_group current_transform_group_for_type current_path current_role current_time current_timestamp current_user cursor cycle data day deallocate declare default deferrable deferred delete depth deref desc describe descriptor deterministic diagnostics disconnect distinct do domain drop dynamic each else elseif end end-exec equals escape except exception exec execute exists exit external fetch first for foreign found from free full function general get global go goto grant group grouping handle having hold hour identity if immediate in indicator initially inner inout input insert intersect into is isolation join key language last lateral leading leave left level like limit local localtime localtimestamp locator loop map match method minute modifies module month names natural nesting new next no none not of old on only open option or order ordinality out outer output overlaps pad parameter partial path prepare preserve primary prior privileges procedure public read reads recursive redo ref references referencing relative release repeat resignal restrict result return returns revoke right role rollback rollup routine row rows savepoint schema scroll search second section select session session_user set sets signal similar size some space specific specifictype sql sqlexception sqlstate sqlwarning start state static system_user table temporary then timezone_hour timezone_minute to trailing transaction translation treat trigger under undo union unique unnest until update usage user using value values view when whenever where while with without work write year zone ",yO={backslashEscapes:!1,hashComments:!1,spaceAfterDashes:!1,slashComments:!1,doubleQuotedStrings:!1,doubleDollarQuotedStrings:!1,unquotedBitLiterals:!1,treatBitsAsBytes:!1,charSetCasts:!1,operatorChars:"*+-%<>!=&|~^/",specialVar:"?",identifierQuotes:'"',words:ot(fi,Si)};function di(e,O,a,t){let r={};for(let i in yO)r[i]=(e.hasOwnProperty(i)?e:yO)[i];return O&&(r.words=ot(O,a||"",t)),r}function Qt(e){return new Z(O=>{var a;let{next:t}=O;if(O.advance(),W(t,Xe)){for(;W(O.next,Xe);)O.advance();O.acceptToken(Lr)}else if(t==36&&O.next==36&&e.doubleDollarQuotedStrings)pi(O),O.acceptToken(N);else if(t==39||t==34&&e.doubleQuotedStrings)Y(O,t,e.backslashEscapes),O.acceptToken(N);else if(t==35&&e.hashComments||t==47&&O.next==47&&e.slashComments)be(O),O.acceptToken(de);else if(t==45&&O.next==45&&(!e.spaceAfterDashes||O.peek(1)==32))be(O),O.acceptToken(de);else if(t==47&&O.next==42){O.advance();for(let r=-1,i=1;!(O.next<0);)if(O.advance(),r==42&&O.next==47){if(i--,!i){O.advance();break}r=-1}else r==47&&O.next==42?(i++,r=-1):r=O.next;O.acceptToken(Jr)}else if((t==101||t==69)&&O.next==39)O.advance(),Y(O,39,!0);else if((t==110||t==78)&&O.next==39&&e.charSetCasts)O.advance(),Y(O,39,e.backslashEscapes),O.acceptToken(N);else if(t==95&&e.charSetCasts)for(let r=0;;r++){if(O.next==39&&r>1){O.advance(),Y(O,39,e.backslashEscapes),O.acceptToken(N);break}if(!xO(O.next))break;O.advance()}else if(t==40)O.acceptToken(Fr);else if(t==41)O.acceptToken(Kr);else if(t==123)O.acceptToken(Oi);else if(t==125)O.acceptToken(ei);else if(t==91)O.acceptToken(ti);else if(t==93)O.acceptToken(ai);else if(t==59)O.acceptToken(ri);else if(e.unquotedBitLiterals&&t==48&&O.next==98)O.advance(),Pe(O),O.acceptToken(ge);else if((t==98||t==66)&&(O.next==39||O.next==34)){const r=O.next;O.advance(),e.treatBitsAsBytes?(Y(O,r,e.backslashEscapes),O.acceptToken(Qi)):(Pe(O,r),O.acceptToken(ge))}else if(t==48&&(O.next==120||O.next==88)||(t==120||t==88)&&O.next==39){let r=O.next==39;for(O.advance();hi(O.next);)O.advance();r&&O.next==39&&O.advance(),O.acceptToken(mO)}else if(t==46&&O.next>=48&&O.next<=57)me(O,!0),O.acceptToken(mO);else if(t==46)O.acceptToken(ii);else if(t>=48&&t<=57)me(O,!1),O.acceptToken(mO);else if(W(t,e.operatorChars)){for(;W(O.next,e.operatorChars);)O.advance();O.acceptToken(si)}else if(W(t,e.specialVar))O.next==t&&O.advance(),ui(O),O.acceptToken(li);else if(W(t,e.identifierQuotes))Y(O,t,!1),O.acceptToken(oi);else if(t==58||t==44)O.acceptToken(ni);else if(xO(t)){let r=lt(O,String.fromCharCode(t));O.acceptToken(O.next==46?$e:(a=e.words[r.toLowerCase()])!==null&&a!==void 0?a:$e)}})}const ct=Qt(yO),$i=T.deserialize({version:1
Line truncated
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import{S as $t,i as gt,s as Pt,e as mt,f as bt,T as E,g as Xt,y as jO,o as Zt,I as xt,J as yt,K as kt,L as vt,C as Yt,M as wt}from"./index-ea3aeb4c.js";import{P as Tt,N as Wt,u as Vt,D as Ut,v as vO,T as D,I as YO,w as iO,x as l,y as _t,L as sO,z as nO,A as R,B as lO,F as ke,G as oO,H as _,J as ve,K as Ye,M as we,E as w,O as z,Q as Ct,R as qt,U as Te,V as b,W as jt,X as Gt,a as C,h as Rt,b as zt,c as At,d as It,e as Et,s as Nt,t as Bt,f as Dt,g as Lt,r as Jt,i as Mt,k as Ht,j as Ft,l as Kt,m as Oa,n as ea,o as ta,p as aa,q as GO,C as N}from"./index-6f704b4c.js";class H{constructor(O,a,t,r,i,s,n,o,Q,p=0,c){this.p=O,this.stack=a,this.state=t,this.reducePos=r,this.pos=i,this.score=s,this.buffer=n,this.bufferBase=o,this.curContext=Q,this.lookAhead=p,this.parent=c}toString(){return`[${this.stack.filter((O,a)=>a%3==0).concat(this.state)}]@${this.pos}${this.score?"!"+this.score:""}`}static start(O,a,t=0){let r=O.parser.context;return new H(O,[],a,t,t,0,[],0,r?new RO(r,r.start):null,0,null)}get context(){return this.curContext?this.curContext.context:null}pushState(O,a){this.stack.push(this.state,a,this.bufferBase+this.buffer.length),this.state=O}reduce(O){var a;let t=O>>19,r=O&65535,{parser:i}=this.p,s=i.dynamicPrecedence(r);if(s&&(this.score+=s),t==0){this.pushState(i.getGoto(this.state,r,!0),this.reducePos),r<i.minRepeatTerm&&this.storeNode(r,this.reducePos,this.reducePos,4,!0),this.reduceContext(r,this.reducePos);return}let n=this.stack.length-(t-1)*3-(O&262144?6:0),o=n?this.stack[n-2]:this.p.ranges[0].from,Q=this.reducePos-o;Q>=2e3&&!(!((a=this.p.parser.nodeSet.types[r])===null||a===void 0)&&a.isAnonymous)&&(o==this.p.lastBigReductionStart?(this.p.bigReductionCount++,this.p.lastBigReductionSize=Q):this.p.lastBigReductionSize<Q&&(this.p.bigReductionCount=1,this.p.lastBigReductionStart=o,this.p.lastBigReductionSize=Q));let p=n?this.stack[n-1]:0,c=this.bufferBase+this.buffer.length-p;if(r<i.minRepeatTerm||O&131072){let u=i.stateFlag(this.state,1)?this.pos:this.reducePos;this.storeNode(r,o,u,c+4,!0)}if(O&262144)this.state=this.stack[n];else{let u=this.stack[n-3];this.state=i.getGoto(u,r,!0)}for(;this.stack.length>n;)this.stack.pop();this.reduceContext(r,o)}storeNode(O,a,t,r=4,i=!1){if(O==0&&(!this.stack.length||this.stack[this.stack.length-1]<this.buffer.length+this.bufferBase)){let s=this,n=this.buffer.length;if(n==0&&s.parent&&(n=s.bufferBase-s.parent.bufferBase,s=s.parent),n>0&&s.buffer[n-4]==0&&s.buffer[n-1]>-1){if(a==t)return;if(s.buffer[n-2]>=a){s.buffer[n-2]=t;return}}}if(!i||this.pos==t)this.buffer.push(O,a,t,r);else{let s=this.buffer.length;if(s>0&&this.buffer[s-4]!=0)for(;s>0&&this.buffer[s-2]>t;)this.buffer[s]=this.buffer[s-4],this.buffer[s+1]=this.buffer[s-3],this.buffer[s+2]=this.buffer[s-2],this.buffer[s+3]=this.buffer[s-1],s-=4,r>4&&(r-=4);this.buffer[s]=O,this.buffer[s+1]=a,this.buffer[s+2]=t,this.buffer[s+3]=r}}shift(O,a,t){let r=this.pos;if(O&131072)this.pushState(O&65535,this.pos);else if(O&262144)this.pos=t,this.shiftContext(a,r),a<=this.p.parser.maxNode&&this.buffer.push(a,r,t,4);else{let i=O,{parser:s}=this.p;(t>this.pos||a<=s.maxNode)&&(this.pos=t,s.stateFlag(i,1)||(this.reducePos=t)),this.pushState(i,r),this.shiftContext(a,r),a<=s.maxNode&&this.buffer.push(a,r,t,4)}}apply(O,a,t){O&65536?this.reduce(O):this.shift(O,a,t)}useNode(O,a){let t=this.p.reused.length-1;(t<0||this.p.reused[t]!=O)&&(this.p.reused.push(O),t++);let r=this.pos;this.reducePos=this.pos=r+O.length,this.pushState(a,r),this.buffer.push(t,r,this.reducePos,-1),this.curContext&&this.updateContext(this.curContext.tracker.reuse(this.curContext.context,O,this,this.p.stream.reset(this.pos-O.length)))}split(){let O=this,a=O.buffer.length;for(;a>0&&O.buffer[a-2]>O.reducePos;)a-=4;let t=O.buffer.slice(a),r=O.bufferBase+a;for(;O&&r==O.bufferBase;)O=O.parent;return new H(this.p,this.stack.slice(),this.state,this.reducePos,this.pos,this.score,t,r,this.curContext,this.lookAhead,O)}recoverByDelete(O,a){let t=O<=this.p.parser.maxNode;t&&this.storeNode(O,this.pos,a,4),this.storeNode(0,this.pos,a,t?8:4),this.pos=this.reducePos=a,this.score-=190}canShift(O){for(let a=new ra(this);;){let t=this.p.parser.stateSlot(a.state,4)||this.p.parser.hasAction(a.state,O);if(t==0)return!1;if(!(t&65536))return!0;a.reduce(t)}}recoverByInsert(O){if(this.stack.length>=300)return[];let a=this.p.parser.nextStates(this.state);if(a.length>4<<1||this.stack.length>=120){let r=[];for(let i=0,s;i<a.length;i+=2)(s=a[i+1])!=this.state&&this.p.parser.hasAction(s,O)&&r.push(a[i],s);if(this.stack.length<120)for(let i=0;r.length<4<<1&&i<a.length;i+=2){let s=a[i+1];r.some((n,o)=>o&1&&n==s)||r.push(a[i],s)}a=r}let t=[];for(let r=0;r<a.length&&t.length<4;r+=2){let i=a[r+1];if(i==this.state)continue;let s=this.split();s.pushState(i,this.pos),s.storeNode(0,s.pos,s.pos,4,!0),s.shiftContext(a[r],this.pos),s.score-=200,t.push(s)}return t}forceReduce(){let O=this.p.parser.stateSlot(this.state,5);if(!(O&65536))return!1;let{parser:a}=this.p;if(!a.validAction(this.state,O)){let t=O>>19,Line truncated
|
||||
\${}
|
||||
} catch (\${error}) {
|
||||
\${}
|
||||
}`,{label:"try",detail:"/ catch block",type:"keyword"}),b("if (${}) {\n ${}\n}",{label:"if",detail:"block",type:"keyword"}),b(`if (\${}) {
|
||||
\${}
|
||||
} else {
|
||||
\${}
|
||||
}`,{label:"if",detail:"/ else block",type:"keyword"}),b(`class \${name} {
|
||||
constructor(\${params}) {
|
||||
\${}
|
||||
}
|
||||
}`,{label:"class",detail:"definition",type:"keyword"}),b('import {${names}} from "${module}"\n${}',{label:"import",detail:"named",type:"keyword"}),b('import ${name} from "${module}"\n${}',{label:"import",detail:"default",type:"keyword"})],he=new ve,Je=new Set(["Script","Block","FunctionExpression","FunctionDeclaration","ArrowFunction","MethodDeclaration","ForStatement"]);function q(e){return(O,a)=>{let t=O.node.getChild("VariableDefinition");return t&&a(t,e),!0}}const Rr=["FunctionDeclaration"],zr={FunctionDeclaration:q("function"),ClassDeclaration:q("class"),ClassExpression:()=>!0,EnumDeclaration:q("constant"),TypeAliasDeclaration:q("type"),NamespaceDeclaration:q("namespace"),VariableDefinition(e,O){e.matchContext(Rr)||O(e,"variable")},TypeDefinition(e,O){O(e,"type")},__proto__:null};function Me(e,O){let a=he.get(O);if(a)return a;let t=[],r=!0;function i(s,n){let o=e.sliceString(s.from,s.to);t.push({label:o,type:n})}return O.cursor(YO.IncludeAnonymous).iterate(s=>{if(r)r=!1;else if(s.name){let n=zr[s.name];if(n&&n(s,i)||Je.has(s.name))return!1}else if(s.to-s.from>8192){for(let n of Me(e,s.node))t.push(n);return!1}}),he.set(O,t),t}const pe=/^[\w$\xa1-\uffff][\w$\d\xa1-\uffff]*$/,He=["TemplateString","String","RegExp","LineComment","BlockComment","VariableDefinition","TypeDefinition","Label","PropertyDefinition","PropertyName","PrivatePropertyDefinition","PrivatePropertyName"];function Ar(e){let O=_(e.state).resolveInner(e.pos,-1);if(He.indexOf(O.name)>-1)return null;let a=O.name=="VariableName"||O.to-O.from<20&&pe.test(e.state.sliceDoc(O.from,O.to));if(!a&&!e.explicit)return null;let t=[];for(let r=O;r;r=r.parent)Je.has(r.name)&&(t=t.concat(Me(e.state.doc,r)));return{options:t,from:a?O.from:e.pos,validFor:pe}}const y=sO.define({name:"javascript",parser:jr.configure({props:[nO.add({IfStatement:R({except:/^\s*({|else\b)/}),TryStatement:R({except:/^\s*({|catch\b|finally\b)/}),LabeledStatement:Ct,SwitchBody:e=>{let O=e.textAfter,a=/^\s*\}/.test(O),t=/^\s*(case|default)\b/.test(O);return e.baseIndent+(a?0:t?1:2)*e.unit},Block:qt({closing:"}"}),ArrowFunction:e=>e.baseIndent+e.unit,"TemplateString BlockComment":()=>null,"Statement Property":R({except:/^{/}),JSXElement(e){let O=/^\s*<\//.test(e.textAfter);return e.lineIndent(e.node.from)+(O?0:e.unit)},JSXEscape(e){let O=/\s*\}/.test(e.textAfter);return e.lineIndent(e.node.from)+(O?0:e.unit)},"JSXOpenTag JSXSelfClosingTag"(e){return e.column(e.node.from)+e.unit}}),lO.add({"Block ClassBody SwitchBody EnumBody ObjectExpression ArrayExpression":ke,BlockComment(e){return{from:e.from+2,to:e.to-2}}})]}),languageData:{closeBrackets:{brackets:["(","[","{","'",'"',"`"]},commentTokens:{line:"//",block:{open:"/*",close:"*/"}},indentOnInput:/^\s*(?:case |default:|\{|\}|<\/)$/,wordChars:"$"}}),Fe={test:e=>/^JSX/.test(e.name),facet:jt({commentTokens:{block:{open:"{/*",close:"*/}"}}})},Ke=y.configure({dialect:"ts"},"typescript"),Ot=y.configure({dialect:"jsx",props:[Te.add(e=>e.isTop?[Fe]:void 0)]}),et=y.configure({dialect:"jsx ts",props:[Te.add(e=>e.isTop?[Fe]:void 0)]},"typescript"),Ir="break case const continue default delete export extends false finally in instanceof let new return static super switch this throw true typeof var yield".split(" ").map(e=>({label:e,type:"keyword"}));function tt(e={}){let O=e.jsx?e.typescript?et:Ot:e.typescript?Ke:y;return new oO(O,[y.data.of({autocomplete:Ye(He,we(Gr.concat(Ir)))}),y.data.of({autocomplete:Ar}),e.jsx?Br:[]])}function Er(e){for(;;){if(e.name=="JSXOpenTag"||e.name=="JSXSelfClosingTag"||e.name=="JSXFragmentTag")return e;if(!e.parent)return null;e=e.parent}}function ue(e,O,a=e.length){for(let t=O==null?void 0:O.firstChild;t;t=t.nextSibling)if(t.name=="JSXIdentifier"||t.name=="JSXBuiltin"||t.name=="JSXNamespacedName"||t.name=="JSXMemberExpression")return e.sliceString(t.from,Math.min(t.to,a));return""}const Nr=typeof navigator=="object"&&/Android\b/.test(navigator.userAgent),Br=w.inputHandler.of((e,O,a,t)=>{if((Nr?e.composing:e.compositionStarted)||e.state.readOnly||O!=a||t!=">"&&t!="/"||!y.isActiveAt(e.state,O,-1))return!1;let{state:r}=e,i=r.changeByRange(s=>{var n,o;let{head:Q}=s,p=_(r).resolveInner(Q,-1),c;if(p.name=="JSXStartTag"&&(p=p.parent),t==">"&&p.name=="JSXFragmentTag")return{range:z.cursor(Q+1),changes:{from:Q,insert:"></>"}};if(t=="/"&&p.name=="JSXFragmentTag"){let u=p.parent,h=u==null?void 0:u.parent;if(u.from==Q-1&&((n=h.lastChild)===null||n===void 0?void 0:n.name)!="JSXEndTag"&&(c=ue(r.doc,h==null?void 0:h.firstChild,Q))){let S=`/${c}>`;return{range:z.cursor(Q+S.length),changes:{from:Q,insert:S}}}}else if(t==">"){let u=Er(p);if(u&&((o=u.lastChild)===null||o===void 0?void 0:o.name)!="JSXEndTag"&&r.sliceDoc(Q,Q+2)!="</"&&(c=ue(r.doc,u,Q)))return{range:z.cursor(Q+1),changes:{from:Q,insert:`></${c}>`}}}return{range:s}});return i.changes.empty?!1:(e.dispatch(i,{userEvent:"input.type",scrollIntoView:!0}),!0)}),j=["_blank","_self","_top","_parent"],$O=["ascii","utf-8","utf-16","latin1","latin1"],gO=["get","post","put","delete"
Line truncated
|
||||
`;function ht(e,O,a){let t=Object.create(null);t.true=t.false=ai,t.null=t.unknown=ri;for(let r of e.split(" "))r&&(t[r]=ot);for(let r of O.split(" "))r&&(t[r]=Qt);for(let r of(a||"").split(" "))r&&(t[r]=$i);return t}const bi="array binary bit boolean char character clob date decimal double float int integer interval large national nchar nclob numeric object precision real smallint time timestamp varchar varying ",Xi="absolute action add after all allocate alter and any are as asc assertion at authorization before begin between both breadth by call cascade cascaded case cast catalog check close collate collation column commit condition connect connection constraint constraints constructor continue corresponding count create cross cube current current_date current_default_transform_group current_transform_group_for_type current_path current_role current_time current_timestamp current_user cursor cycle data day deallocate declare default deferrable deferred delete depth deref desc describe descriptor deterministic diagnostics disconnect distinct do domain drop dynamic each else elseif end end-exec equals escape except exception exec execute exists exit external fetch first for foreign found from free full function general get global go goto grant group grouping handle having hold hour identity if immediate in indicator initially inner inout input insert intersect into is isolation join key language last lateral leading leave left level like limit local localtime localtimestamp locator loop map match method minute modifies module month names natural nesting new next no none not of old on only open option or order ordinality out outer output overlaps pad parameter partial path prepare preserve primary prior privileges procedure public read reads recursive redo ref references referencing relative release repeat resignal restrict result return returns revoke right role rollback rollup routine row rows savepoint schema scroll search second section select session session_user set sets signal similar size some space specific specifictype sql sqlexception sqlstate sqlwarning start state static system_user table temporary then timezone_hour timezone_minute to trailing transaction translation treat trigger under undo union unique unnest until update usage user using value values view when whenever where while with without work write year zone ",yO={backslashEscapes:!1,hashComments:!1,spaceAfterDashes:!1,slashComments:!1,doubleQuotedStrings:!1,doubleDollarQuotedStrings:!1,unquotedBitLiterals:!1,treatBitsAsBytes:!1,charSetCasts:!1,operatorChars:"*+-%<>!=&|~^/",specialVar:"?",identifierQuotes:'"',words:ht(Xi,bi)};function Zi(e,O,a,t){let r={};for(let i in yO)r[i]=(e.hasOwnProperty(i)?e:yO)[i];return O&&(r.words=ht(O,a||"",t)),r}function pt(e){return new Z(O=>{var a;let{next:t}=O;if(O.advance(),W(t,Ze)){for(;W(O.next,Ze);)O.advance();O.acceptToken(ei)}else if(t==36&&O.next==36&&e.doubleDollarQuotedStrings)Pi(O),O.acceptToken(B);else if(t==39||t==34&&e.doubleQuotedStrings)Y(O,t,e.backslashEscapes),O.acceptToken(B);else if(t==35&&e.hashComments||t==47&&O.next==47&&e.slashComments)Xe(O),O.acceptToken($e);else if(t==45&&O.next==45&&(!e.spaceAfterDashes||O.peek(1)==32))Xe(O),O.acceptToken($e);else if(t==47&&O.next==42){O.advance();for(let r=-1,i=1;!(O.next<0);)if(O.advance(),r==42&&O.next==47){if(i--,!i){O.advance();break}r=-1}else r==47&&O.next==42?(i++,r=-1):r=O.next;O.acceptToken(ti)}else if((t==101||t==69)&&O.next==39)O.advance(),Y(O,39,!0);else if((t==110||t==78)&&O.next==39&&e.charSetCasts)O.advance(),Y(O,39,e.backslashEscapes),O.acceptToken(B);else if(t==95&&e.charSetCasts)for(let r=0;;r++){if(O.next==39&&r>1){O.advance(),Y(O,39,e.backslashEscapes),O.acceptToken(B);break}if(!xO(O.next))break;O.advance()}else if(t==40)O.acceptToken(ii);else if(t==41)O.acceptToken(si);else if(t==123)O.acceptToken(ni);else if(t==125)O.acceptToken(li);else if(t==91)O.acceptToken(oi);else if(t==93)O.acceptToken(Qi);else if(t==59)O.acceptToken(ci);else if(e.unquotedBitLiterals&&t==48&&O.next==98)O.advance(),me(O),O.acceptToken(Pe);else if((t==98||t==66)&&(O.next==39||O.next==34)){const r=O.next;O.advance(),e.treatBitsAsBytes?(Y(O,r,e.backslashEscapes),O.acceptToken(di)):(me(O,r),O.acceptToken(Pe))}else if(t==48&&(O.next==120||O.next==88)||(t==120||t==88)&&O.next==39){let r=O.next==39;for(O.advance();gi(O.next);)O.advance();r&&O.next==39&&O.advance(),O.acceptToken(mO)}else if(t==46&&O.next>=48&&O.next<=57)be(O,!0),O.acceptToken(mO);else if(t==46)O.acceptToken(hi);else if(t>=48&&t<=57)be(O,!1),O.acceptToken(mO);else if(W(t,e.operatorChars)){for(;W(O.next,e.operatorChars);)O.advance();O.acceptToken(pi)}else if(W(t,e.specialVar))O.next==t&&O.advance(),mi(O),O.acceptToken(fi);else if(W(t,e.identifierQuotes))Y(O,t,!1),O.acceptToken(Si);else if(t==58||t==44)O.acceptToken(ui);else if(xO(t)){let r=ct(O,String.fromCharCode(t));O.acceptToken(O.next==46?ge:(a=e.words[r.toLowerCase()])!==null&&a!==void 0?a:ge)}})}const ut=pt(yO),xi=T.deserialize({version:1
Line truncated
|
||||
Loaded 100 of 233 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user