Compare commits

...
74 Commits
Author SHA1 Message Date
Gani Georgiev ff1f99436a prevent closing the datefield options on calendar select and updated collections upsert save handler 2023-03-28 09:06:16 +03:00
Gani Georgiev 3ea02c945d updated changelog and formatting 2023-03-28 07:59:37 +03:00
Gani Georgiev 4c903684d8 added linux cgo target 2023-03-27 21:39:46 +03:00
Gani Georgiev d0239f25ed updated changelog and ui/dist 2023-03-27 19:44:09 +03:00
Gani Georgiev 05adb8c018 update ui deps 2023-03-27 16:17:37 +03:00
Gani Georgiev c901c9ab7d updated go deps 2023-03-27 16:16:09 +03:00
Gani Georgiev c9fba9972a cleanup old remaining temp views 2023-03-27 16:07:47 +03:00
Gani Georgiev 2c40811fac added required field marker 2023-03-27 16:07:20 +03:00
Gani Georgiev b81112f82e minor ui adjustments 2023-03-26 22:49:33 +03:00
Gani Georgiev 0acf8198a4 revert record upsert changes on upload failure 2023-03-26 22:49:18 +03:00
Gani Georgiev 5ece360808 autoselect new field names 2023-03-26 20:32:41 +03:00
Gani Georgiev 3a5d3d521f added ProviderName and ProviderClient fields to core.RecordAuthWithOAuth2Event 2023-03-26 19:32:23 +03:00
Gani Georgiev f024de3cc4 updated ui/dist 2023-03-26 19:19:44 +03:00
Gani Georgiev a4abe9e2cb removed unused AuthProviderConfig.Meta field 2023-03-25 23:34:00 +02:00
Gani Georgiev 5678339af0 added migrate history-sync command 2023-03-25 21:48:19 +02:00
Gani Georgiev e5a22b8bd8 added a flag indicating OAuth2 auth record creation 2023-03-25 15:18:28 +02:00
Gani Georgiev be0ee7f66c fixed generateAppleClientSecret call 2023-03-24 22:48:32 +02:00
Gani Georgiev 78a9f7429c updated ui/dist 2023-03-24 22:30:57 +02:00
Gani Georgiev d8b4daf47f updated rule field styles 2023-03-23 22:59:02 +02:00
Gani Georgiev 7a2360d785 [#2029] enable any id aliased column in a view query to be detected as relation field 2023-03-23 19:51:06 +02:00
Gani Georgiev 67ecebe935 [#1939] removed redundant COALESCE normalizations 2023-03-23 19:30:35 +02:00
Gani Georgiev 5cbdbf6c10 updated ui/dist 2023-03-23 16:15:19 +02:00
Gani Georgiev 0710214701 added new field indicator 2023-03-23 10:42:27 +02:00
Gani Georgiev 9736a45e80 renamed daos.GetTableColumns and daos.GetTableInfo for consistency 2023-03-22 17:15:17 +02:00
Gani Georgiev 923fc26a31 changed types.JsonArray to support generics 2023-03-22 17:12:44 +02:00
Gani Georgiev a79f3a7c56 removed test js file 2023-03-22 16:53:34 +02:00
Gani Georgiev 8430944650 deprecated SchemaField.Unique 2023-03-22 15:47:34 +02:00
Gani Georgiev 9b54fd3516 added debug log for already committed response error 2023-03-22 15:42:35 +02:00
Gani Georgiev 226352f884 replaced indirect expand field.Unique with unique index constraint check 2023-03-21 21:38:28 +02:00
Gani Georgiev 2a682a10b2 preload tinymce 2023-03-21 18:52:13 +02:00
Gani Georgiev b67549209a [#2118] added option to explicitly set the record id from the Admin UI 2023-03-21 17:56:31 +02:00
Gani Georgiev 1126a0e16f updated changelog and ui/dist 2023-03-21 15:55:42 +02:00
Gani Georgiev 17472cb40a minor internal indexes handling adjustments and test 2023-03-21 15:31:20 +02:00
Gani Georgiev 981de64c7f added Index.Build helper method 2023-03-21 14:26:44 +02:00
Gani Georgiev 1b45e23c81 removed unnecessary helper method and updated index parser regex 2023-03-19 22:15:18 +02:00
Gani Georgiev 29621303df updated indexes column migration to load unique fields and custom indexes 2023-03-19 22:13:52 +02:00
Gani Georgiev a0ec5707d1 (no tests) collection indexes scaffoldings 2023-03-19 16:18:33 +02:00
Gani Georgiev 695c20a969 decreased bcrypt round hash for admins to 12 for consistency with the auth records 2023-03-19 16:10:11 +02:00
Gani Georgiev 95bb2eb871 update automigrate templates to check collection indexes 2023-03-19 16:02:29 +02:00
Gani Georgiev 971916c20d removed unnecessary mail layou css class 2023-03-19 10:16:23 +02:00
Gani Georgiev 44f5172db7 added create index sql parser 2023-03-19 10:15:26 +02:00
Gani Georgiev 5fd103481c updated ui/dist 2023-03-19 10:14:44 +02:00
Gani Georgiev 82abd7e0b0 minor collection ui adjustments 2023-03-19 08:17:36 +02:00
Gani Georgiev 361cfb56b7 [#1904] simplified default mail template styles 2023-03-16 22:35:03 +02:00
Gani Georgiev 4d16d0e16e new schema and indexes ui 2023-03-16 19:21:16 +02:00
Gani Georgiev 254e691e92 [#2072] registered RemoveTrailingSlash middleware only for the /api/* routes 2023-03-15 18:09:49 +02:00
Gani Georgiev e735d9d21b synced with master 2023-03-14 02:04:34 +02:00
Gani Georgiev 31b24ecb86 removed eager unique collection name check to allow lazy validation during bulk import 2023-03-14 01:31:30 +02:00
Gani Georgiev 3eb3d0bbdf lowered auth record bcrypt round factor to 12 2023-03-13 15:52:51 +02:00
Gani Georgiev f180af7af7 synced with master 2023-03-12 18:09:16 +02:00
Gani Georgiev d046811df7 optimized single relation lookups 2023-03-07 23:28:35 +02:00
Gani Georgiev 4768e07c0b added multiple->single value conversion warning 2023-03-06 23:24:50 +02:00
Gani Georgiev 0547202382 use nonconcurrent db when deleting log requests 2023-03-06 23:24:31 +02:00
Gani Georgiev 684f91f7e5 added indirect view update by source collection field change 2023-03-06 20:10:18 +02:00
Gani Georgiev bce4094134 updated changelog 2023-03-06 15:32:46 +02:00
Gani Georgiev 6a60bc1df3 meged apple-oauth2 2023-03-06 15:30:35 +02:00
Gani Georgiev 19b7a7a74f updated ui/dist 2023-03-06 15:29:03 +02:00
Gani Georgiev 8de0e61cf4 synced with master 2023-03-06 15:27:17 +02:00
Gani Georgiev 5344ec83fa normalized values on maxSelect change 2023-03-06 15:20:07 +02:00
Gani Georgiev 65aa114103 added google OAuth2 verified_email check 2023-03-05 19:19:11 +02:00
Gani Georgiev 8728161288 sync with latest changes 2023-03-05 16:16:07 +02:00
Gani Georgiev 2420b2804a synced with master 2023-03-05 16:12:51 +02:00
Gani Georgiev 2791628d96 minor updates to the rule fields ui 2023-03-05 15:18:51 +02:00
Gani Georgiev 01f0af7af6 added errors slide transition 2023-03-04 13:06:24 +02:00
Gani Georgiev e8d61e7b45 simplified rules ui 2023-03-04 13:05:55 +02:00
Gani Georgiev a5ac83c7b0 use jwt.ParseECPrivateKeyFromPEM instead of the custom one 2023-03-02 21:31:27 +02:00
Gani Georgiev a67c14c368 added support for @request.headers.* 2023-03-02 18:56:18 +02:00
Gani Georgiev 19faa0d8e7 updated calendar styles 2023-03-02 16:10:56 +02:00
Gani Georgiev 56a77fd5ff updated migrations comment 2023-03-02 15:17:29 +02:00
Gani Georgiev 07727dbde6 [#1956] normalized _requests.method to UPPERCASE 2023-03-02 15:15:00 +02:00
Gani Georgiev a3d26a73c3 removed unnecessary struct pointer 2023-03-02 14:07:46 +02:00
Gani Georgiev b328827705 added generate-client-secret api test 2023-03-01 23:45:54 +02:00
Gani Georgiev f5e5fae773 added apple oauth2 integration 2023-03-01 23:29:51 +02:00
Gani Georgiev 41f01bab0d fixed collections import message typo 2023-02-28 16:50:25 +02:00
233 changed files with 7174 additions and 9415 deletions

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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*
+60
View File
@@ -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
View File
@@ -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
}
+73
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+7
View File
@@ -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
View File
@@ -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,
})
}
+122
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+54
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+87
View File
@@ -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)
}
+117
View File
@@ -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
View File
@@ -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
+44 -10
View File
@@ -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
}
}
}
}
+24 -2
View File
@@ -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: `{
+15 -6
View File
@@ -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
View File
@@ -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
}
+48
View File
@@ -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())
}
}
}
-11
View File
@@ -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 {
-100
View File
@@ -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{
+39 -39
View File
@@ -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
+355 -233
View File
File diff suppressed because it is too large. Load diff
+13 -45
View File
@@ -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>
`
+7 -6
View File
@@ -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
})
}
+136
View File
@@ -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
})
}
+2 -2
View File
@@ -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
View File
@@ -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
}
+5 -4
View File
@@ -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"`
+5 -5
View File
@@ -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
View File
@@ -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
}
+1
View File
@@ -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"`
}
+37 -6
View File
@@ -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+
+67
View File
@@ -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)
}
}
}
+7
View File
@@ -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,
}
}
+8
View File
@@ -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) {
+1
View File
@@ -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{
+32
View File
@@ -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{
+29
View File
@@ -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
+46 -25
View File
@@ -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
// ---
}
+5 -4
View File
@@ -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])
}
+37 -19
View File
@@ -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.
+222
View File
@@ -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)
}
+9
View File
@@ -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)
}
+9
View File
@@ -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
View File
@@ -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),
)
}
+17
View File
@@ -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{}
+2
View File
@@ -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",
+2
View File
@@ -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,
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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,
+2
View File
@@ -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
View File
@@ -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
}
+2
View File
@@ -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,
+2
View File
@@ -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",
+2
View File
@@ -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,
+2
View File
@@ -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",
+2
View File
@@ -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
+2
View File
@@ -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",
},
+2
View File
@@ -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,
+2
View File
@@ -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",
+194
View File
@@ -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
}
+216
View File
@@ -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)
}
}
}
+1 -1
View File
@@ -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 {
+23
View File
@@ -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
}
+50
View File
@@ -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
View File
@@ -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
View File
@@ -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 {:") +
".+" +
+4 -4
View File
@@ -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",
},
},
{
+10 -9
View File
@@ -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:
+14 -13
View File
@@ -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
-27
View File
@@ -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);
});
+1 -1
View File
@@ -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"
@@ -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,
@@ -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.",
@@ -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]}');
@@ -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&&lt(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)&&lt(l,"active",e[3]===e[8].code)},i(p){m||(tt(i.$$.fragment,p),m=!0)},o(p){et(i.$$.fragment,p),m=!1},d(p){p&&c(l),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&&lt(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)&&lt(l,"active",e[3]===e[8].code)},i(p){m||(tt(i.$$.fragment,p),m=!0)},o(p){et(i.$$.fragment,p),m=!1},d(p){p&&c(l),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
View File
@@ -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
View File
@@ -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