mirror of
https://github.com/pocketbase/pocketbase.git
synced 2026-09-20 13:30:50 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c2cd5a534 | ||
|
|
5d8fc939e2 | ||
|
|
1a5180d7d3 | ||
|
|
8a08a4764d | ||
|
|
66b317f01c | ||
|
|
ab5a770346 | ||
|
|
65697add43 | ||
|
|
841415f0ff | ||
|
|
f295ce9403 | ||
|
|
383b2a1279 | ||
|
|
73fb12c2bc | ||
|
|
571c4dcc8d |
@@ -34,6 +34,9 @@ jobs:
|
||||
# - name: Run linter
|
||||
# uses: golangci/golangci-lint-action@v3
|
||||
|
||||
- name: Run tests
|
||||
run: go test ./...
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
with:
|
||||
|
||||
+24
-8
@@ -4,11 +4,14 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
func TestAdminAuth(t *testing.T) {
|
||||
@@ -92,6 +95,7 @@ func TestAdminRequestPasswordReset(t *testing.T) {
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/admins/request-password-reset",
|
||||
Body: strings.NewReader(`{"email":"missing@example.com"}`),
|
||||
Delay: 100 * time.Millisecond,
|
||||
ExpectedStatus: 204,
|
||||
},
|
||||
{
|
||||
@@ -99,22 +103,34 @@ func TestAdminRequestPasswordReset(t *testing.T) {
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/admins/request-password-reset",
|
||||
Body: strings.NewReader(`{"email":"test@example.com"}`),
|
||||
Delay: 100 * time.Millisecond,
|
||||
ExpectedStatus: 204,
|
||||
// usually this events are fired but since the submit is
|
||||
// executed in a separate go routine they are fired async
|
||||
// ExpectedEvents: map[string]int{
|
||||
// "OnModelBeforeUpdate": 1,
|
||||
// "OnModelAfterUpdate": 1,
|
||||
// "OnMailerBeforeUserResetPasswordSend:1": 1,
|
||||
// "OnMailerAfterUserResetPasswordSend:1": 1,
|
||||
// },
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnModelBeforeUpdate": 1,
|
||||
"OnModelAfterUpdate": 1,
|
||||
"OnMailerBeforeAdminResetPasswordSend": 1,
|
||||
"OnMailerAfterAdminResetPasswordSend": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "existing admin (after already sent)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/admins/request-password-reset",
|
||||
Body: strings.NewReader(`{"email":"test@example.com"}`),
|
||||
Delay: 100 * time.Millisecond,
|
||||
ExpectedStatus: 204,
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
// simulate recent password request
|
||||
admin, err := app.Dao().FindAdminByEmail("test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
admin.LastResetSentAt = types.NowDateTime()
|
||||
dao := daos.New(app.Dao().DB()) // new dao to ignore hooks
|
||||
if err := dao.Save(admin); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+13
-8
@@ -288,19 +288,24 @@ func (api *realtimeApi) broadcastRecord(action string, record *models.Record) er
|
||||
return nil // no subscribers
|
||||
}
|
||||
|
||||
// remove the expand from the broadcasted record because we don't
|
||||
// know if the clients have access to view the expanded records
|
||||
cleanRecord := *record
|
||||
cleanRecord.SetExpand(nil)
|
||||
|
||||
subscriptionRuleMap := map[string]*string{
|
||||
(collection.Name + "/" + record.Id): collection.ViewRule,
|
||||
(collection.Id + "/" + record.Id): collection.ViewRule,
|
||||
collection.Name: collection.ListRule,
|
||||
collection.Id: collection.ListRule,
|
||||
(collection.Name + "/" + cleanRecord.Id): collection.ViewRule,
|
||||
(collection.Id + "/" + cleanRecord.Id): collection.ViewRule,
|
||||
collection.Name: collection.ListRule,
|
||||
collection.Id: collection.ListRule,
|
||||
}
|
||||
|
||||
recordData := &recordData{
|
||||
data := &recordData{
|
||||
Action: action,
|
||||
Record: record,
|
||||
Record: &cleanRecord,
|
||||
}
|
||||
|
||||
serializedData, err := json.Marshal(recordData)
|
||||
serializedData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
if api.app.IsDebug() {
|
||||
log.Println(err)
|
||||
@@ -314,7 +319,7 @@ func (api *realtimeApi) broadcastRecord(action string, record *models.Record) er
|
||||
continue
|
||||
}
|
||||
|
||||
if !api.canAccessRecord(client, record, rule) {
|
||||
if !api.canAccessRecord(client, data.Record, rule) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
+55
-19
@@ -52,10 +52,9 @@ func (api *recordApi) list(c echo.Context) error {
|
||||
return rest.NewForbiddenError("Only admins can perform this action.", nil)
|
||||
}
|
||||
|
||||
// forbid user/guest defined non-relational joins (aka. @collection.*)
|
||||
queryStr := c.QueryString()
|
||||
if admin == nil && queryStr != "" && (strings.Contains(queryStr, "@collection") || strings.Contains(queryStr, "%40collection")) {
|
||||
return rest.NewForbiddenError("Only admins can filter by @collection.", nil)
|
||||
// forbid users and guests to query special filter/sort fields
|
||||
if err := api.checkForForbiddenQueryFields(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
requestData := api.exportRequestData(c)
|
||||
@@ -63,14 +62,15 @@ func (api *recordApi) list(c echo.Context) error {
|
||||
fieldsResolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData)
|
||||
|
||||
searchProvider := search.NewProvider(fieldsResolver).
|
||||
Query(api.app.Dao().RecordQuery(collection))
|
||||
Query(api.app.Dao().RecordQuery(collection)).
|
||||
CountColumn(fmt.Sprintf("%s.id", api.app.Dao().DB().QuoteSimpleColumnName(collection.Name)))
|
||||
|
||||
if admin == nil && collection.ListRule != nil {
|
||||
searchProvider.AddFilter(search.FilterData(*collection.ListRule))
|
||||
}
|
||||
|
||||
var rawRecords = []dbx.NullStringMap{}
|
||||
result, err := searchProvider.ParseAndExec(queryStr, &rawRecords)
|
||||
result, err := searchProvider.ParseAndExec(c.QueryString(), &rawRecords)
|
||||
if err != nil {
|
||||
return rest.NewBadRequestError("Invalid filter parameters.", err)
|
||||
}
|
||||
@@ -80,13 +80,13 @@ func (api *recordApi) list(c echo.Context) error {
|
||||
// expand records relations
|
||||
expands := strings.Split(c.QueryParam(expandQueryParam), ",")
|
||||
if len(expands) > 0 {
|
||||
expandErr := api.app.Dao().ExpandRecords(
|
||||
failed := api.app.Dao().ExpandRecords(
|
||||
records,
|
||||
expands,
|
||||
api.expandFunc(c, requestData),
|
||||
)
|
||||
if expandErr != nil && api.app.IsDebug() {
|
||||
log.Println("Failed to expand relations: ", expandErr)
|
||||
if len(failed) > 0 && api.app.IsDebug() {
|
||||
log.Println("Failed to expand relations: ", failed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,16 +141,14 @@ func (api *recordApi) view(c echo.Context) error {
|
||||
return rest.NewNotFoundError("", fetchErr)
|
||||
}
|
||||
|
||||
expands := strings.Split(c.QueryParam(expandQueryParam), ",")
|
||||
if len(expands) > 0 {
|
||||
expandErr := api.app.Dao().ExpandRecord(
|
||||
record,
|
||||
expands,
|
||||
api.expandFunc(c, requestData),
|
||||
)
|
||||
if expandErr != nil && api.app.IsDebug() {
|
||||
log.Println("Failed to expand relations: ", expandErr)
|
||||
}
|
||||
// expand record relations
|
||||
failed := api.app.Dao().ExpandRecord(
|
||||
record,
|
||||
strings.Split(c.QueryParam(expandQueryParam), ","),
|
||||
api.expandFunc(c, requestData),
|
||||
)
|
||||
if len(failed) > 0 && api.app.IsDebug() {
|
||||
log.Println("Failed to expand relations: ", failed)
|
||||
}
|
||||
|
||||
event := &core.RecordViewEvent{
|
||||
@@ -226,6 +224,16 @@ func (api *recordApi) create(c echo.Context) error {
|
||||
return rest.NewBadRequestError("Failed to create record.", err)
|
||||
}
|
||||
|
||||
// expand record relations
|
||||
failed := api.app.Dao().ExpandRecord(
|
||||
e.Record,
|
||||
strings.Split(e.HttpContext.QueryParam(expandQueryParam), ","),
|
||||
api.expandFunc(e.HttpContext, requestData),
|
||||
)
|
||||
if len(failed) > 0 && api.app.IsDebug() {
|
||||
log.Println("Failed to expand relations: ", failed)
|
||||
}
|
||||
|
||||
return e.HttpContext.JSON(http.StatusOK, e.Record)
|
||||
})
|
||||
}
|
||||
@@ -296,6 +304,16 @@ func (api *recordApi) update(c echo.Context) error {
|
||||
return rest.NewBadRequestError("Failed to update record.", err)
|
||||
}
|
||||
|
||||
// expand record relations
|
||||
failed := api.app.Dao().ExpandRecord(
|
||||
e.Record,
|
||||
strings.Split(e.HttpContext.QueryParam(expandQueryParam), ","),
|
||||
api.expandFunc(e.HttpContext, requestData),
|
||||
)
|
||||
if len(failed) > 0 && api.app.IsDebug() {
|
||||
log.Println("Failed to expand relations: ", failed)
|
||||
}
|
||||
|
||||
return e.HttpContext.JSON(http.StatusOK, e.Record)
|
||||
})
|
||||
}
|
||||
@@ -389,6 +407,24 @@ func (api *recordApi) exportRequestData(c echo.Context) map[string]any {
|
||||
return result
|
||||
}
|
||||
|
||||
func (api *recordApi) checkForForbiddenQueryFields(c echo.Context) error {
|
||||
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
|
||||
if admin != nil {
|
||||
return nil // admins are allowed to query everything
|
||||
}
|
||||
|
||||
decodedQuery := c.QueryParam(search.FilterQueryParam) + c.QueryParam(search.SortQueryParam)
|
||||
forbiddenFields := []string{"@collection.", "@request."}
|
||||
|
||||
for _, field := range forbiddenFields {
|
||||
if strings.Contains(decodedQuery, field) {
|
||||
return rest.NewForbiddenError("Only admins can filter by @collection and @request query params", nil)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *recordApi) expandFunc(c echo.Context, requestData map[string]any) daos.ExpandFetchFunc {
|
||||
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
|
||||
|
||||
|
||||
+95
-24
@@ -127,7 +127,7 @@ func TestRecordsList(t *testing.T) {
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "expand",
|
||||
Name: "expand relations",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/collections/demo2/records?expand=manyrels,onerel&perPage=2&sort=created",
|
||||
RequestHeaders: map[string]string{
|
||||
@@ -139,11 +139,12 @@ func TestRecordsList(t *testing.T) {
|
||||
`"perPage":2`,
|
||||
`"totalItems":2`,
|
||||
`"items":[{`,
|
||||
`"@expand":{`,
|
||||
`"id":"577bd676-aacb-4072-b7da-99d00ee210a4"`,
|
||||
`"id":"848a1dea-5ddd-42d6-a00d-030547bffcfe"`,
|
||||
`"manyrels":[{`,
|
||||
`"manyrels":[]`,
|
||||
`"rel_cascade":"`,
|
||||
`"cascaderel":"`,
|
||||
`"onerel":{"@collectionId":"3f2888f8-075d-49fe-9d09-ea7e951000dc","@collectionName":"demo",`,
|
||||
`"json":[1,2,3]`,
|
||||
`"select":["a","b"]`,
|
||||
@@ -356,6 +357,7 @@ func TestRecordView(t *testing.T) {
|
||||
`"@collectionId":"2c1010aa-b8fe-41d9-a980-99534ca8a167"`,
|
||||
`"@collectionName":"demo2"`,
|
||||
`"id":"63c2ab80-84ab-4057-a592-4604a731f78f"`,
|
||||
`"@expand":{`,
|
||||
`"manyrels":[{`,
|
||||
`"onerel":{`,
|
||||
`"@collectionId":"3f2888f8-075d-49fe-9d09-ea7e951000dc"`,
|
||||
@@ -454,8 +456,8 @@ func TestRecordDelete(t *testing.T) {
|
||||
"OnRecordAfterDeleteRequest": 1,
|
||||
"OnModelAfterUpdate": 1, // nullify related record
|
||||
"OnModelBeforeUpdate": 1, // nullify related record
|
||||
"OnModelBeforeDelete": 3, // +1 cascade delete related record
|
||||
"OnModelAfterDelete": 3, // +1 cascade delete related record
|
||||
"OnModelBeforeDelete": 3, // +2 cascade delete related records
|
||||
"OnModelAfterDelete": 3, // +2 cascade delete related records
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
ensureDeletedFiles(app, "3f2888f8-075d-49fe-9d09-ea7e951000dc", "577bd676-aacb-4072-b7da-99d00ee210a4")
|
||||
@@ -476,8 +478,8 @@ func TestRecordDelete(t *testing.T) {
|
||||
"OnRecordAfterDeleteRequest": 1,
|
||||
"OnModelAfterUpdate": 1, // nullify related record
|
||||
"OnModelBeforeUpdate": 1, // nullify related record
|
||||
"OnModelBeforeDelete": 3, // +1 cascade delete related record
|
||||
"OnModelAfterDelete": 3, // +1 cascade delete related record
|
||||
"OnModelBeforeDelete": 3, // +2 cascade delete related records
|
||||
"OnModelAfterDelete": 3, // +2 cascade delete related records
|
||||
},
|
||||
AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
ensureDeletedFiles(app, "3f2888f8-075d-49fe-9d09-ea7e951000dc", "577bd676-aacb-4072-b7da-99d00ee210a4")
|
||||
@@ -650,7 +652,7 @@ func TestRecordCreate(t *testing.T) {
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/collections/demo2/records",
|
||||
Body: strings.NewReader(`{
|
||||
"rel_cascade": "577bd676-aacb-4072-b7da-99d00ee210a4",
|
||||
"cascaderel": "577bd676-aacb-4072-b7da-99d00ee210a4",
|
||||
"onerel": "577bd676-aacb-4072-b7da-99d00ee210a4",
|
||||
"manyrels": ["577bd676-aacb-4072-b7da-99d00ee210a4"],
|
||||
"text": "test123",
|
||||
@@ -665,13 +667,14 @@ func TestRecordCreate(t *testing.T) {
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "user submit in restricted collection (rule pass check)",
|
||||
Name: "user submit in restricted collection (rule pass check) + expand relations",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/collections/demo2/records",
|
||||
Url: "/api/collections/demo2/records?expand=missing,onerel,manyrels,selfrel",
|
||||
Body: strings.NewReader(`{
|
||||
"rel_cascade":"577bd676-aacb-4072-b7da-99d00ee210a4",
|
||||
"cascaderel":"577bd676-aacb-4072-b7da-99d00ee210a4",
|
||||
"onerel":"577bd676-aacb-4072-b7da-99d00ee210a4",
|
||||
"manyrels":["577bd676-aacb-4072-b7da-99d00ee210a4"],
|
||||
"selfrel":"63c2ab80-84ab-4057-a592-4604a731f78f",
|
||||
"text":"test123",
|
||||
"bool":true,
|
||||
"number":1
|
||||
@@ -683,12 +686,21 @@ func TestRecordCreate(t *testing.T) {
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"id":`,
|
||||
`"rel_cascade":"577bd676-aacb-4072-b7da-99d00ee210a4"`,
|
||||
`"cascaderel":"577bd676-aacb-4072-b7da-99d00ee210a4"`,
|
||||
`"onerel":"577bd676-aacb-4072-b7da-99d00ee210a4"`,
|
||||
`"manyrels":["577bd676-aacb-4072-b7da-99d00ee210a4"]`,
|
||||
`"selfrel":"63c2ab80-84ab-4057-a592-4604a731f78f"`,
|
||||
`"text":"test123"`,
|
||||
`"bool":true`,
|
||||
`"number":1`,
|
||||
`"@expand":{`,
|
||||
`"selfrel":{`,
|
||||
`"id":"63c2ab80-84ab-4057-a592-4604a731f78f"`,
|
||||
},
|
||||
NotExpectedContent: []string{
|
||||
// user don't have access to view the below expands
|
||||
`"manyrels":[{`,
|
||||
`"onerel":{`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnRecordBeforeCreateRequest": 1,
|
||||
@@ -698,13 +710,14 @@ func TestRecordCreate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "admin submit in restricted collection (rule skip check)",
|
||||
Name: "admin submit in restricted collection (rule skip check) + expand relations",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/collections/demo2/records",
|
||||
Url: "/api/collections/demo2/records?expand=missing,onerel,manyrels,selfrel",
|
||||
Body: strings.NewReader(`{
|
||||
"rel_cascade": "577bd676-aacb-4072-b7da-99d00ee210a4",
|
||||
"cascaderel": "577bd676-aacb-4072-b7da-99d00ee210a4",
|
||||
"onerel": "577bd676-aacb-4072-b7da-99d00ee210a4",
|
||||
"manyrels" :["577bd676-aacb-4072-b7da-99d00ee210a4"],
|
||||
"manyrels":["577bd676-aacb-4072-b7da-99d00ee210a4"],
|
||||
"selfrel":"94568ca2-0bee-49d7-b749-06cb97956fd9",
|
||||
"text": "test123",
|
||||
"bool": false,
|
||||
"number": 1
|
||||
@@ -715,12 +728,20 @@ func TestRecordCreate(t *testing.T) {
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"id":`,
|
||||
`"rel_cascade":"577bd676-aacb-4072-b7da-99d00ee210a4"`,
|
||||
`"cascaderel":"577bd676-aacb-4072-b7da-99d00ee210a4"`,
|
||||
`"onerel":"577bd676-aacb-4072-b7da-99d00ee210a4"`,
|
||||
`"manyrels":["577bd676-aacb-4072-b7da-99d00ee210a4"]`,
|
||||
`"text":"test123"`,
|
||||
`"bool":false`,
|
||||
`"number":1`,
|
||||
`"@expand":{`,
|
||||
`"manyrels":[{`,
|
||||
`"onerel":{`,
|
||||
`"selfrel":{`,
|
||||
`"@collectionId":"3f2888f8-075d-49fe-9d09-ea7e951000dc"`,
|
||||
`"@collectionName":"demo"`,
|
||||
`"id":"577bd676-aacb-4072-b7da-99d00ee210a4"`,
|
||||
`"id":"94568ca2-0bee-49d7-b749-06cb97956fd9"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnRecordBeforeCreateRequest": 1,
|
||||
@@ -844,12 +865,13 @@ func TestRecordUpdate(t *testing.T) {
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "user submit in restricted collection (rule pass check)",
|
||||
Name: "user submit in restricted collection (rule pass check) + expand relations",
|
||||
Method: http.MethodPatch,
|
||||
Url: "/api/collections/demo2/records/63c2ab80-84ab-4057-a592-4604a731f78f",
|
||||
Url: "/api/collections/demo2/records/63c2ab80-84ab-4057-a592-4604a731f78f?expand=missing,onerel,manyrels,selfrel",
|
||||
Body: strings.NewReader(`{
|
||||
"text":"test_new",
|
||||
"bool":false
|
||||
"selfrel":"63c2ab80-84ab-4057-a592-4604a731f78f",
|
||||
"bool":true
|
||||
}`),
|
||||
RequestHeaders: map[string]string{
|
||||
// test3@example.com
|
||||
@@ -858,11 +880,19 @@ func TestRecordUpdate(t *testing.T) {
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"id":"63c2ab80-84ab-4057-a592-4604a731f78f"`,
|
||||
`"rel_cascade":"577bd676-aacb-4072-b7da-99d00ee210a4"`,
|
||||
`"cascaderel":"577bd676-aacb-4072-b7da-99d00ee210a4"`,
|
||||
`"onerel":"848a1dea-5ddd-42d6-a00d-030547bffcfe"`,
|
||||
`"manyrels":["848a1dea-5ddd-42d6-a00d-030547bffcfe","577bd676-aacb-4072-b7da-99d00ee210a4"]`,
|
||||
`"bool":false`,
|
||||
`"bool":true`,
|
||||
`"text":"test_new"`,
|
||||
`"selfrel":"63c2ab80-84ab-4057-a592-4604a731f78f"`,
|
||||
`"@expand":{`,
|
||||
`"selfrel":{`,
|
||||
},
|
||||
NotExpectedContent: []string{
|
||||
// user don't have access to view the below expands
|
||||
`"manyrels":[{`,
|
||||
`"onerel":{`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnRecordBeforeUpdateRequest": 1,
|
||||
@@ -872,12 +902,44 @@ func TestRecordUpdate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "admin submit in restricted collection (rule skip check)",
|
||||
Name: "user submit in restricted collection (rule pass check) + expand relations (no view rule access when bool is false)",
|
||||
Method: http.MethodPatch,
|
||||
Url: "/api/collections/demo2/records/63c2ab80-84ab-4057-a592-4604a731f78f",
|
||||
Url: "/api/collections/demo2/records/63c2ab80-84ab-4057-a592-4604a731f78f?expand=missing,onerel,manyrels,selfrel",
|
||||
Body: strings.NewReader(`{
|
||||
"selfrel":"63c2ab80-84ab-4057-a592-4604a731f78f",
|
||||
"bool":false
|
||||
}`),
|
||||
RequestHeaders: map[string]string{
|
||||
// test3@example.com
|
||||
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImVtYWlsIjoidGVzdDNAZXhhbXBsZS5jb20iLCJpZCI6Ijk3Y2MzZDNkLTZiYTItMzgzZi1iNDJhLTdiYzg0ZDI3NDEwYyIsImV4cCI6MTg5MzUxNTU3Nn0.Q965uvlTxxOsZbACXSgJQNXykYK0TKZ87nyPzemvN4E",
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"id":"63c2ab80-84ab-4057-a592-4604a731f78f"`,
|
||||
`"bool":false`,
|
||||
`"selfrel":"63c2ab80-84ab-4057-a592-4604a731f78f"`,
|
||||
},
|
||||
NotExpectedContent: []string{
|
||||
`"@expand":{`,
|
||||
`"manyrels":[{`, // admin only
|
||||
`"onerel":{`, // admin only
|
||||
`"selfrel":{`, // bool=true view rule
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnRecordBeforeUpdateRequest": 1,
|
||||
"OnRecordAfterUpdateRequest": 1,
|
||||
"OnModelBeforeUpdate": 1,
|
||||
"OnModelAfterUpdate": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "admin submit in restricted collection (rule skip check) + expand relations",
|
||||
Method: http.MethodPatch,
|
||||
Url: "/api/collections/demo2/records/63c2ab80-84ab-4057-a592-4604a731f78f?expand=onerel,manyrels,selfrel,missing",
|
||||
Body: strings.NewReader(`{
|
||||
"text":"test_new",
|
||||
"number":1
|
||||
"number":1,
|
||||
"selfrel":"94568ca2-0bee-49d7-b749-06cb97956fd9"
|
||||
}`),
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
|
||||
@@ -887,6 +949,15 @@ func TestRecordUpdate(t *testing.T) {
|
||||
`"id":"63c2ab80-84ab-4057-a592-4604a731f78f"`,
|
||||
`"text":"test_new"`,
|
||||
`"number":1`,
|
||||
`"@expand":{`,
|
||||
`"manyrels":[{`,
|
||||
`"onerel":{`,
|
||||
`"selfrel":{`,
|
||||
`"@collectionId":"3f2888f8-075d-49fe-9d09-ea7e951000dc"`,
|
||||
`"@collectionName":"demo"`,
|
||||
`"id":"848a1dea-5ddd-42d6-a00d-030547bffcfe"`,
|
||||
`"id":"577bd676-aacb-4072-b7da-99d00ee210a4"`,
|
||||
`"id":"94568ca2-0bee-49d7-b749-06cb97956fd9"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnRecordBeforeUpdateRequest": 1,
|
||||
|
||||
+58
-21
@@ -4,9 +4,12 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
func TestUsersAuthMethods(t *testing.T) {
|
||||
@@ -132,6 +135,7 @@ func TestUserRequestPasswordReset(t *testing.T) {
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/users/request-password-reset",
|
||||
Body: strings.NewReader(`{"email":"missing@example.com"}`),
|
||||
Delay: 100 * time.Millisecond,
|
||||
ExpectedStatus: 204,
|
||||
},
|
||||
{
|
||||
@@ -139,22 +143,34 @@ func TestUserRequestPasswordReset(t *testing.T) {
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/users/request-password-reset",
|
||||
Body: strings.NewReader(`{"email":"test@example.com"}`),
|
||||
Delay: 100 * time.Millisecond,
|
||||
ExpectedStatus: 204,
|
||||
// usually this events are fired but since the submit is
|
||||
// executed in a separate go routine they are fired async
|
||||
// ExpectedEvents: map[string]int{
|
||||
// "OnModelBeforeUpdate": 1,
|
||||
// "OnModelAfterUpdate": 1,
|
||||
// "OnMailerBeforeUserResetPasswordSend": 1,
|
||||
// "OnMailerAfterUserResetPasswordSend": 1,
|
||||
// },
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnModelBeforeUpdate": 1,
|
||||
"OnModelAfterUpdate": 1,
|
||||
"OnMailerBeforeUserResetPasswordSend": 1,
|
||||
"OnMailerAfterUserResetPasswordSend": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "existing user (after already sent)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/users/request-password-reset",
|
||||
Body: strings.NewReader(`{"email":"test@example.com"}`),
|
||||
Delay: 100 * time.Millisecond,
|
||||
ExpectedStatus: 204,
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
// simulate recent password request
|
||||
user, err := app.Dao().FindUserByEmail("test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
user.LastResetSentAt = types.NowDateTime()
|
||||
dao := daos.New(app.Dao().DB()) // new dao to ignore hooks
|
||||
if err := dao.Save(user); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -216,50 +232,71 @@ func TestUserConfirmPasswordReset(t *testing.T) {
|
||||
|
||||
func TestUserRequestVerification(t *testing.T) {
|
||||
scenarios := []tests.ApiScenario{
|
||||
// empty data
|
||||
{
|
||||
Name: "empty data",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/users/request-verification",
|
||||
Body: strings.NewReader(``),
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."}}`},
|
||||
},
|
||||
// invalid data
|
||||
{
|
||||
Name: "invalid data",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/users/request-verification",
|
||||
Body: strings.NewReader(`{"email`),
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
// missing user
|
||||
{
|
||||
Name: "missing user",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/users/request-verification",
|
||||
Body: strings.NewReader(`{"email":"missing@example.com"}`),
|
||||
Delay: 100 * time.Millisecond,
|
||||
ExpectedStatus: 204,
|
||||
},
|
||||
// existing already verified user
|
||||
{
|
||||
Name: "existing already verified user",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/users/request-verification",
|
||||
Body: strings.NewReader(`{"email":"test@example.com"}`),
|
||||
Delay: 100 * time.Millisecond,
|
||||
ExpectedStatus: 204,
|
||||
},
|
||||
// existing unverified user
|
||||
{
|
||||
Name: "existing unverified user",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/users/request-verification",
|
||||
Body: strings.NewReader(`{"email":"test2@example.com"}`),
|
||||
Delay: 100 * time.Millisecond,
|
||||
ExpectedStatus: 204,
|
||||
// usually this events are fired but since the submit is
|
||||
// executed in a separate go routine they are fired async
|
||||
// ExpectedEvents: map[string]int{
|
||||
// "OnModelBeforeUpdate": 1,
|
||||
// "OnModelAfterUpdate": 1,
|
||||
// "OnMailerBeforeUserVerificationSend": 1,
|
||||
// "OnMailerAfterUserVerificationSend": 1,
|
||||
// },
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnModelBeforeUpdate": 1,
|
||||
"OnModelAfterUpdate": 1,
|
||||
"OnMailerBeforeUserVerificationSend": 1,
|
||||
"OnMailerAfterUserVerificationSend": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "existing unverified user (after already sent)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/users/request-verification",
|
||||
Body: strings.NewReader(`{"email":"test2@example.com"}`),
|
||||
Delay: 100 * time.Millisecond,
|
||||
ExpectedStatus: 204,
|
||||
BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
// simulate recent verification sent
|
||||
user, err := app.Dao().FindUserByEmail("test2@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
user.LastVerificationSentAt = types.NowDateTime()
|
||||
dao := daos.New(app.Dao().DB()) // new dao to ignore hooks
|
||||
if err := dao.Save(user); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -347,6 +347,7 @@ func (app *BaseApp) NewFilesystem() (*filesystem.System, error) {
|
||||
app.settings.S3.Endpoint,
|
||||
app.settings.S3.AccessKey,
|
||||
app.settings.S3.Secret,
|
||||
app.settings.S3.ForcePathStyle,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+7
-6
@@ -244,12 +244,13 @@ func (c SmtpConfig) Validate() error {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type S3Config struct {
|
||||
Enabled bool `form:"enabled" json:"enabled"`
|
||||
Bucket string `form:"bucket" json:"bucket"`
|
||||
Region string `form:"region" json:"region"`
|
||||
Endpoint string `form:"endpoint" json:"endpoint"`
|
||||
AccessKey string `form:"accessKey" json:"accessKey"`
|
||||
Secret string `form:"secret" json:"secret"`
|
||||
Enabled bool `form:"enabled" json:"enabled"`
|
||||
Bucket string `form:"bucket" json:"bucket"`
|
||||
Region string `form:"region" json:"region"`
|
||||
Endpoint string `form:"endpoint" json:"endpoint"`
|
||||
AccessKey string `form:"accessKey" json:"accessKey"`
|
||||
Secret string `form:"secret" json:"secret"`
|
||||
ForcePathStyle bool `form:"forcePathStyle" json:"forcePathStyle"`
|
||||
}
|
||||
|
||||
// Validate makes S3Config validatable by implementing [validation.Validatable] interface.
|
||||
|
||||
@@ -172,7 +172,7 @@ func TestSettingsRedactClone(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := `{"meta":{"appName":"test123","appUrl":"http://localhost:8090","senderName":"Support","senderAddress":"support@example.com","userVerificationUrl":"%APP_URL%/_/#/users/confirm-verification/%TOKEN%","userResetPasswordUrl":"%APP_URL%/_/#/users/confirm-password-reset/%TOKEN%","userConfirmEmailChangeUrl":"%APP_URL%/_/#/users/confirm-email-change/%TOKEN%"},"logs":{"maxDays":7},"smtp":{"enabled":false,"host":"smtp.example.com","port":587,"username":"","password":"******","tls":true},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","secret":"******"},"adminAuthToken":{"secret":"******","duration":1209600},"adminPasswordResetToken":{"secret":"******","duration":1800},"userAuthToken":{"secret":"******","duration":1209600},"userPasswordResetToken":{"secret":"******","duration":1800},"userEmailChangeToken":{"secret":"******","duration":1800},"userVerificationToken":{"secret":"******","duration":604800},"emailAuth":{"enabled":true,"exceptDomains":null,"onlyDomains":null,"minPasswordLength":8},"googleAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"facebookAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"githubAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"gitlabAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"}}`
|
||||
expected := `{"meta":{"appName":"test123","appUrl":"http://localhost:8090","senderName":"Support","senderAddress":"support@example.com","userVerificationUrl":"%APP_URL%/_/#/users/confirm-verification/%TOKEN%","userResetPasswordUrl":"%APP_URL%/_/#/users/confirm-password-reset/%TOKEN%","userConfirmEmailChangeUrl":"%APP_URL%/_/#/users/confirm-email-change/%TOKEN%"},"logs":{"maxDays":7},"smtp":{"enabled":false,"host":"smtp.example.com","port":587,"username":"","password":"******","tls":true},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","secret":"******","forcePathStyle":false},"adminAuthToken":{"secret":"******","duration":1209600},"adminPasswordResetToken":{"secret":"******","duration":1800},"userAuthToken":{"secret":"******","duration":1209600},"userPasswordResetToken":{"secret":"******","duration":1800},"userEmailChangeToken":{"secret":"******","duration":1800},"userVerificationToken":{"secret":"******","duration":604800},"emailAuth":{"enabled":true,"exceptDomains":null,"onlyDomains":null,"minPasswordLength":8},"googleAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"facebookAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"githubAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"gitlabAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"}}`
|
||||
|
||||
if encodedStr := string(encoded); encodedStr != expected {
|
||||
t.Fatalf("Expected %v, got \n%v", expected, encodedStr)
|
||||
|
||||
@@ -113,7 +113,7 @@ func TestFindCollectionReferences(t *testing.T) {
|
||||
t.Fatalf("Expected 1 collection, got %d: %v", len(result), result)
|
||||
}
|
||||
|
||||
expectedFields := []string{"onerel", "manyrels", "rel_cascade"}
|
||||
expectedFields := []string{"onerel", "manyrels", "cascaderel"}
|
||||
|
||||
for col, fields := range result {
|
||||
if col.Name != "demo2" {
|
||||
|
||||
@@ -150,6 +150,10 @@ func (dao *Dao) IsRecordValueUnique(
|
||||
// FindUserRelatedRecords returns all records that has a reference
|
||||
// to the provided User model (via the user shema field).
|
||||
func (dao *Dao) FindUserRelatedRecords(user *models.User) ([]*models.Record, error) {
|
||||
if user.Id == "" {
|
||||
return []*models.Record{}, nil
|
||||
}
|
||||
|
||||
collections, err := dao.FindCollectionsWithUserFields()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+10
-4
@@ -17,21 +17,27 @@ const MaxExpandDepth = 6
|
||||
type ExpandFetchFunc func(relCollection *models.Collection, relIds []string) ([]*models.Record, error)
|
||||
|
||||
// ExpandRecord expands the relations of a single Record model.
|
||||
func (dao *Dao) ExpandRecord(record *models.Record, expands []string, fetchFunc ExpandFetchFunc) error {
|
||||
//
|
||||
// Returns a map with the failed expand parameters and their errors.
|
||||
func (dao *Dao) ExpandRecord(record *models.Record, expands []string, fetchFunc ExpandFetchFunc) map[string]error {
|
||||
return dao.ExpandRecords([]*models.Record{record}, expands, fetchFunc)
|
||||
}
|
||||
|
||||
// ExpandRecords expands the relations of the provided Record models list.
|
||||
func (dao *Dao) ExpandRecords(records []*models.Record, expands []string, fetchFunc ExpandFetchFunc) error {
|
||||
//
|
||||
// Returns a map with the failed expand parameters and their errors.
|
||||
func (dao *Dao) ExpandRecords(records []*models.Record, expands []string, fetchFunc ExpandFetchFunc) map[string]error {
|
||||
normalized := normalizeExpands(expands)
|
||||
|
||||
failed := map[string]error{}
|
||||
|
||||
for _, expand := range normalized {
|
||||
if err := dao.expandRecords(records, expand, fetchFunc, 1); err != nil {
|
||||
return err
|
||||
failed[expand] = err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return failed
|
||||
}
|
||||
|
||||
// notes:
|
||||
|
||||
+35
-37
@@ -19,11 +19,11 @@ func TestExpandRecords(t *testing.T) {
|
||||
col, _ := app.Dao().FindCollectionByNameOrId("demo4")
|
||||
|
||||
scenarios := []struct {
|
||||
recordIds []string
|
||||
expands []string
|
||||
fetchFunc daos.ExpandFetchFunc
|
||||
expectExpandProps int
|
||||
expectError bool
|
||||
recordIds []string
|
||||
expands []string
|
||||
fetchFunc daos.ExpandFetchFunc
|
||||
expectExpandProps int
|
||||
expectExpandFailures int
|
||||
}{
|
||||
// empty records
|
||||
{
|
||||
@@ -33,7 +33,7 @@ func TestExpandRecords(t *testing.T) {
|
||||
return app.Dao().FindRecordsByIds(c, ids, nil)
|
||||
},
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
},
|
||||
// empty expand
|
||||
{
|
||||
@@ -43,7 +43,7 @@ func TestExpandRecords(t *testing.T) {
|
||||
return app.Dao().FindRecordsByIds(c, ids, nil)
|
||||
},
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
},
|
||||
// empty fetchFunc
|
||||
{
|
||||
@@ -51,7 +51,7 @@ func TestExpandRecords(t *testing.T) {
|
||||
[]string{"onerel", "manyrels.onerel.manyrels"},
|
||||
nil,
|
||||
0,
|
||||
true,
|
||||
2,
|
||||
},
|
||||
// fetchFunc with error
|
||||
{
|
||||
@@ -61,7 +61,7 @@ func TestExpandRecords(t *testing.T) {
|
||||
return nil, errors.New("test error")
|
||||
},
|
||||
0,
|
||||
true,
|
||||
2,
|
||||
},
|
||||
// invalid missing first level expand
|
||||
{
|
||||
@@ -71,7 +71,7 @@ func TestExpandRecords(t *testing.T) {
|
||||
return app.Dao().FindRecordsByIds(c, ids, nil)
|
||||
},
|
||||
0,
|
||||
true,
|
||||
1,
|
||||
},
|
||||
// invalid missing second level expand
|
||||
{
|
||||
@@ -81,7 +81,7 @@ func TestExpandRecords(t *testing.T) {
|
||||
return app.Dao().FindRecordsByIds(c, ids, nil)
|
||||
},
|
||||
0,
|
||||
true,
|
||||
1,
|
||||
},
|
||||
// expand normalizations
|
||||
{
|
||||
@@ -96,7 +96,7 @@ func TestExpandRecords(t *testing.T) {
|
||||
return app.Dao().FindRecordsByIds(c, ids, nil)
|
||||
},
|
||||
9,
|
||||
false,
|
||||
0,
|
||||
},
|
||||
// single expand
|
||||
{
|
||||
@@ -111,7 +111,7 @@ func TestExpandRecords(t *testing.T) {
|
||||
return app.Dao().FindRecordsByIds(c, ids, nil)
|
||||
},
|
||||
2,
|
||||
false,
|
||||
0,
|
||||
},
|
||||
// maxExpandDepth reached
|
||||
{
|
||||
@@ -121,18 +121,17 @@ func TestExpandRecords(t *testing.T) {
|
||||
return app.Dao().FindRecordsByIds(c, ids, nil)
|
||||
},
|
||||
6,
|
||||
false,
|
||||
0,
|
||||
},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
ids := list.ToUniqueStringSlice(s.recordIds)
|
||||
records, _ := app.Dao().FindRecordsByIds(col, ids, nil)
|
||||
err := app.Dao().ExpandRecords(records, s.expands, s.fetchFunc)
|
||||
failed := app.Dao().ExpandRecords(records, s.expands, s.fetchFunc)
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err)
|
||||
if len(failed) != s.expectExpandFailures {
|
||||
t.Errorf("(%d) Expected %d failures, got %d: \n%v", i, s.expectExpandFailures, len(failed), failed)
|
||||
}
|
||||
|
||||
encoded, _ := json.Marshal(records)
|
||||
@@ -140,7 +139,7 @@ func TestExpandRecords(t *testing.T) {
|
||||
totalExpandProps := strings.Count(encodedStr, "@expand")
|
||||
|
||||
if s.expectExpandProps != totalExpandProps {
|
||||
t.Errorf("(%d) Expected %d @expand props in %v, got %d", i, s.expectExpandProps, encodedStr, totalExpandProps)
|
||||
t.Errorf("(%d) Expected %d @expand props, got %d: \n%v", i, s.expectExpandProps, totalExpandProps, encodedStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,11 +151,11 @@ func TestExpandRecord(t *testing.T) {
|
||||
col, _ := app.Dao().FindCollectionByNameOrId("demo4")
|
||||
|
||||
scenarios := []struct {
|
||||
recordId string
|
||||
expands []string
|
||||
fetchFunc daos.ExpandFetchFunc
|
||||
expectExpandProps int
|
||||
expectError bool
|
||||
recordId string
|
||||
expands []string
|
||||
fetchFunc daos.ExpandFetchFunc
|
||||
expectExpandProps int
|
||||
expectExpandFailures int
|
||||
}{
|
||||
// empty expand
|
||||
{
|
||||
@@ -166,7 +165,7 @@ func TestExpandRecord(t *testing.T) {
|
||||
return app.Dao().FindRecordsByIds(c, ids, nil)
|
||||
},
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
},
|
||||
// empty fetchFunc
|
||||
{
|
||||
@@ -174,7 +173,7 @@ func TestExpandRecord(t *testing.T) {
|
||||
[]string{"onerel", "manyrels.onerel.manyrels"},
|
||||
nil,
|
||||
0,
|
||||
true,
|
||||
2,
|
||||
},
|
||||
// fetchFunc with error
|
||||
{
|
||||
@@ -184,7 +183,7 @@ func TestExpandRecord(t *testing.T) {
|
||||
return nil, errors.New("test error")
|
||||
},
|
||||
0,
|
||||
true,
|
||||
2,
|
||||
},
|
||||
// invalid missing first level expand
|
||||
{
|
||||
@@ -194,7 +193,7 @@ func TestExpandRecord(t *testing.T) {
|
||||
return app.Dao().FindRecordsByIds(c, ids, nil)
|
||||
},
|
||||
0,
|
||||
true,
|
||||
1,
|
||||
},
|
||||
// invalid missing second level expand
|
||||
{
|
||||
@@ -204,7 +203,7 @@ func TestExpandRecord(t *testing.T) {
|
||||
return app.Dao().FindRecordsByIds(c, ids, nil)
|
||||
},
|
||||
0,
|
||||
true,
|
||||
1,
|
||||
},
|
||||
// expand normalizations
|
||||
{
|
||||
@@ -214,7 +213,7 @@ func TestExpandRecord(t *testing.T) {
|
||||
return app.Dao().FindRecordsByIds(c, ids, nil)
|
||||
},
|
||||
3,
|
||||
false,
|
||||
0,
|
||||
},
|
||||
// single expand
|
||||
{
|
||||
@@ -224,7 +223,7 @@ func TestExpandRecord(t *testing.T) {
|
||||
return app.Dao().FindRecordsByIds(c, ids, nil)
|
||||
},
|
||||
1,
|
||||
false,
|
||||
0,
|
||||
},
|
||||
// maxExpandDepth reached
|
||||
{
|
||||
@@ -234,17 +233,16 @@ func TestExpandRecord(t *testing.T) {
|
||||
return app.Dao().FindRecordsByIds(c, ids, nil)
|
||||
},
|
||||
6,
|
||||
false,
|
||||
0,
|
||||
},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
record, _ := app.Dao().FindFirstRecordByData(col, "id", s.recordId)
|
||||
err := app.Dao().ExpandRecord(record, s.expands, s.fetchFunc)
|
||||
failed := app.Dao().ExpandRecord(record, s.expands, s.fetchFunc)
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err)
|
||||
if len(failed) != s.expectExpandFailures {
|
||||
t.Errorf("(%d) Expected %d failures, got %d: \n%v", i, s.expectExpandFailures, len(failed), failed)
|
||||
}
|
||||
|
||||
encoded, _ := json.Marshal(record)
|
||||
@@ -252,7 +250,7 @@ func TestExpandRecord(t *testing.T) {
|
||||
totalExpandProps := strings.Count(encodedStr, "@expand")
|
||||
|
||||
if s.expectExpandProps != totalExpandProps {
|
||||
t.Errorf("(%d) Expected %d @expand props in %v, got %d", i, s.expectExpandProps, encodedStr, totalExpandProps)
|
||||
t.Errorf("(%d) Expected %d @expand props, got %d: \n%v", i, s.expectExpandProps, totalExpandProps, encodedStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,6 +298,7 @@ func TestFindUserRelatedRecords(t *testing.T) {
|
||||
}
|
||||
|
||||
if len(records) != len(scenario.expectedIds) {
|
||||
fmt.Println(records[0])
|
||||
t.Errorf("(%d) Expected %d records, got %d (%v)", i, len(scenario.expectedIds), len(records), records)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
var _ search.FieldResolver = (*RecordFieldResolver)(nil)
|
||||
|
||||
type join struct {
|
||||
id string
|
||||
table string
|
||||
on dbx.Expression
|
||||
}
|
||||
@@ -36,7 +37,7 @@ type RecordFieldResolver struct {
|
||||
baseCollection *models.Collection
|
||||
allowedFields []string
|
||||
requestData map[string]any
|
||||
joins map[string]join
|
||||
joins []join // we cannot use a map because the insertion order is not preserved
|
||||
loadedCollections []*models.Collection
|
||||
}
|
||||
|
||||
@@ -50,7 +51,7 @@ func NewRecordFieldResolver(
|
||||
dao: dao,
|
||||
baseCollection: baseCollection,
|
||||
requestData: requestData,
|
||||
joins: make(map[string]join),
|
||||
joins: []join{},
|
||||
loadedCollections: []*models.Collection{baseCollection},
|
||||
allowedFields: []string{
|
||||
`^\w+[\w\.]*$`,
|
||||
@@ -85,6 +86,7 @@ func (r *RecordFieldResolver) UpdateQuery(query *dbx.SelectQuery) error {
|
||||
// id
|
||||
// project.screen.status
|
||||
// @request.status
|
||||
// @request.user.profile.someRelation.name
|
||||
// @collection.product.name
|
||||
func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, placeholderParams dbx.Params, err error) {
|
||||
if len(r.allowedFields) > 0 && !list.ExistInSliceWithRegex(fieldName, r.allowedFields) {
|
||||
@@ -93,15 +95,6 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
|
||||
props := strings.Split(fieldName, ".")
|
||||
|
||||
// check for @request field
|
||||
if props[0] == "@request" {
|
||||
if len(props) == 1 {
|
||||
return "", nil, fmt.Errorf("Invalid @request data field path in %q.", fieldName)
|
||||
}
|
||||
|
||||
return r.resolveRequestField(props[1:]...)
|
||||
}
|
||||
|
||||
currentCollectionName := r.baseCollection.Name
|
||||
currentTableAlias := currentCollectionName
|
||||
|
||||
@@ -113,16 +106,54 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
}
|
||||
|
||||
currentCollectionName = props[1]
|
||||
currentTableAlias = "c_" + currentCollectionName
|
||||
currentTableAlias = "__collection_" + currentCollectionName
|
||||
|
||||
collection, err := r.loadCollection(currentCollectionName)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("Failed to load collection %q from field path %q.", currentCollectionName, fieldName)
|
||||
}
|
||||
|
||||
r.addJoin(collection.Name, currentTableAlias, "", "", "")
|
||||
r.addJoin(collection.Name, currentTableAlias, nil)
|
||||
|
||||
props = props[2:] // leave only the collection fields
|
||||
} else if props[0] == "@request" {
|
||||
// check for @request field
|
||||
if len(props) == 1 {
|
||||
return "", nil, fmt.Errorf("Invalid @request data field path in %q.", fieldName)
|
||||
}
|
||||
|
||||
// not a profile relational field
|
||||
if len(props) <= 4 || !strings.HasPrefix(fieldName, "@request.user.profile.") {
|
||||
return r.resolveStaticRequestField(props[1:]...)
|
||||
}
|
||||
|
||||
// resolve the profile collection fields
|
||||
currentCollectionName = models.ProfileCollectionName
|
||||
currentTableAlias = "__user_" + currentCollectionName
|
||||
|
||||
collection, err := r.loadCollection(currentCollectionName)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("Failed to load collection %q from field path %q.", currentCollectionName, fieldName)
|
||||
}
|
||||
|
||||
profileIdPlaceholder, profileIdPlaceholderParam, err := r.resolveStaticRequestField("user", "profile", "id")
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("Failed to resolve @request.user.profile.id path in %q.", fieldName)
|
||||
}
|
||||
if strings.ToLower(profileIdPlaceholder) == "null" {
|
||||
// the user doesn't have an associated profile
|
||||
return "NULL", nil, nil
|
||||
}
|
||||
|
||||
// join the profile collection
|
||||
r.addJoin(collection.Name, currentTableAlias, dbx.NewExp(fmt.Sprintf(
|
||||
// aka. profiles.id = profileId
|
||||
"[[%s.id]] = %s",
|
||||
inflector.Columnify(currentTableAlias),
|
||||
profileIdPlaceholder,
|
||||
), profileIdPlaceholderParam))
|
||||
|
||||
props = props[3:] // leave only the profile fields
|
||||
}
|
||||
|
||||
baseModelFields := schema.ReservedFieldNames()
|
||||
@@ -173,9 +204,14 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
r.addJoin(
|
||||
newCollectionName,
|
||||
newTableAlias,
|
||||
"id",
|
||||
currentTableAlias,
|
||||
field.Name,
|
||||
dbx.NewExp(fmt.Sprintf(
|
||||
// 'LIKE' expr is used to handle the case when the reference field supports multiple values (aka. is json array)
|
||||
"[[%s.%s]] LIKE ('%%' || [[%s.%s]] || '%%')",
|
||||
inflector.Columnify(currentTableAlias),
|
||||
inflector.Columnify(field.Name),
|
||||
inflector.Columnify(newTableAlias),
|
||||
inflector.Columnify("id"),
|
||||
)),
|
||||
)
|
||||
|
||||
currentCollectionName = newCollectionName
|
||||
@@ -185,7 +221,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
return "", nil, fmt.Errorf("Failed to resolve field %q.", fieldName)
|
||||
}
|
||||
|
||||
func (r *RecordFieldResolver) resolveRequestField(path ...string) (resultName string, placeholderParams dbx.Params, err error) {
|
||||
func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (resultName string, placeholderParams dbx.Params, err error) {
|
||||
// ignore error because requestData is dynamic and some of the
|
||||
// lookup keys may not be defined for the request
|
||||
resultVal, _ := extractNestedMapVal(r.requestData, path...)
|
||||
@@ -259,24 +295,27 @@ func (r *RecordFieldResolver) loadCollection(collectionNameOrId string) (*models
|
||||
return collection, nil
|
||||
}
|
||||
|
||||
func (r *RecordFieldResolver) addJoin(tableName, tableAlias, fieldName, ref, refFieldName string) {
|
||||
table := fmt.Sprintf(
|
||||
func (r *RecordFieldResolver) addJoin(tableName string, tableAlias string, on dbx.Expression) {
|
||||
tableExpr := fmt.Sprintf(
|
||||
"%s %s",
|
||||
inflector.Columnify(tableName),
|
||||
inflector.Columnify(tableAlias),
|
||||
)
|
||||
|
||||
var on dbx.Expression
|
||||
if ref != "" {
|
||||
on = dbx.NewExp(fmt.Sprintf(
|
||||
// 'LIKE' expr is used to handle the case when the reference field supports multiple values (aka. is json array)
|
||||
"[[%s.%s]] LIKE ('%%' || [[%s.%s]] || '%%')",
|
||||
inflector.Columnify(ref),
|
||||
inflector.Columnify(refFieldName),
|
||||
inflector.Columnify(tableAlias),
|
||||
inflector.Columnify(fieldName),
|
||||
))
|
||||
join := join{
|
||||
id: tableAlias,
|
||||
table: tableExpr,
|
||||
on: on,
|
||||
}
|
||||
|
||||
r.joins[tableAlias] = join{table, on}
|
||||
// replace existing join
|
||||
for i, j := range r.joins {
|
||||
if j.id == join.id {
|
||||
r.joins[i] = join
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// register new join
|
||||
r.joins = append(r.joins, join)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ package resolvers_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/resolvers"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
)
|
||||
|
||||
func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
@@ -18,54 +19,94 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
requestData := map[string]any{
|
||||
"user": map[string]any{
|
||||
"id": "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c",
|
||||
"profile": map[string]any{
|
||||
"id": "d13f60a4-5765-48c7-9e1d-3e782340f833",
|
||||
"name": "test",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
fieldName string
|
||||
expectQueryParts []string // we are matching parts of the query
|
||||
// since joins are added with map iteration and the order is not guaranteed
|
||||
name string
|
||||
fields []string
|
||||
expectQuery string
|
||||
}{
|
||||
// missing field
|
||||
{"", []string{
|
||||
{
|
||||
"missing field",
|
||||
[]string{""},
|
||||
"SELECT `demo4`.* FROM `demo4`",
|
||||
}},
|
||||
// non relation field
|
||||
{"title", []string{
|
||||
},
|
||||
{
|
||||
"non relation field",
|
||||
[]string{"title"},
|
||||
"SELECT `demo4`.* FROM `demo4`",
|
||||
}},
|
||||
// incomplete rel
|
||||
{"onerel", []string{
|
||||
},
|
||||
{
|
||||
"incomplete rel",
|
||||
[]string{"onerel"},
|
||||
"SELECT `demo4`.* FROM `demo4`",
|
||||
}},
|
||||
// single rel
|
||||
{"onerel.title", []string{
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4`",
|
||||
" LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')",
|
||||
}},
|
||||
// nested incomplete rels
|
||||
{"manyrels.onerel", []string{
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4`",
|
||||
" LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%')",
|
||||
}},
|
||||
// nested complete rels
|
||||
{"manyrels.onerel.title", []string{
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4`",
|
||||
" LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%')",
|
||||
" LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel.id]] || '%')",
|
||||
}},
|
||||
// // repeated nested rels
|
||||
{"manyrels.onerel.manyrels.onerel.title", []string{
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4`",
|
||||
" LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%')",
|
||||
" LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel.id]] || '%')",
|
||||
" LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels` ON [[demo4_manyrels_onerel.manyrels]] LIKE ('%' || [[demo4_manyrels_onerel_manyrels.id]] || '%')",
|
||||
" LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels_onerel` ON [[demo4_manyrels_onerel_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel_manyrels_onerel.id]] || '%')",
|
||||
}},
|
||||
},
|
||||
{
|
||||
"single rel",
|
||||
[]string{"onerel.title"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')",
|
||||
},
|
||||
{
|
||||
"non-relation field + single rel",
|
||||
[]string{"title", "onerel.title"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')",
|
||||
},
|
||||
{
|
||||
"nested incomplete rels",
|
||||
[]string{"manyrels.onerel"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%')",
|
||||
},
|
||||
{
|
||||
"nested complete rels",
|
||||
[]string{"manyrels.onerel.title"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel.id]] || '%')",
|
||||
},
|
||||
{
|
||||
"repeated nested rels",
|
||||
[]string{"manyrels.onerel.manyrels.onerel.title"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels` ON [[demo4_manyrels_onerel.manyrels]] LIKE ('%' || [[demo4_manyrels_onerel_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels_onerel` ON [[demo4_manyrels_onerel_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel_manyrels_onerel.id]] || '%')",
|
||||
},
|
||||
{
|
||||
"multiple rels",
|
||||
[]string{"manyrels.title", "onerel.onefile"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')",
|
||||
},
|
||||
{
|
||||
"@collection join",
|
||||
[]string{"@collection.demo.title", "@collection.demo2.text", "@collection.demo.file"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo` `__collection_demo` LEFT JOIN `demo2` `__collection_demo2`",
|
||||
},
|
||||
{
|
||||
"static @request.user.profile fields",
|
||||
[]string{"@request.user.id", "@request.user.profile.id", "@request.data.demo"},
|
||||
"SELECT `demo4`.* FROM `demo4`",
|
||||
},
|
||||
{
|
||||
"relational @request.user.profile fields",
|
||||
[]string{"@request.user.profile.rel.id", "@request.user.profile.rel.name"},
|
||||
"^" +
|
||||
regexp.QuoteMeta("SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `profiles` `__user_profiles` ON [[__user_profiles.id]] =") +
|
||||
" {:.*} " +
|
||||
regexp.QuoteMeta("LEFT JOIN `profiles` `__user_profiles_rel` ON [[__user_profiles.rel]] LIKE ('%' || [[__user_profiles_rel.id]] || '%')") +
|
||||
"$",
|
||||
},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
query := app.Dao().RecordQuery(collection)
|
||||
|
||||
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, nil)
|
||||
r.Resolve(s.fieldName)
|
||||
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData)
|
||||
for _, field := range s.fields {
|
||||
r.Resolve(field)
|
||||
}
|
||||
|
||||
if err := r.UpdateQuery(query); err != nil {
|
||||
t.Errorf("(%d) UpdateQuery failed with error %v", i, err)
|
||||
@@ -74,16 +115,8 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
|
||||
rawQuery := query.Build().SQL()
|
||||
|
||||
partsLength := 0
|
||||
for _, part := range s.expectQueryParts {
|
||||
partsLength += len(part)
|
||||
if !strings.Contains(rawQuery, part) {
|
||||
t.Errorf("(%d) Part %v is missing from query \n%v", i, part, rawQuery)
|
||||
}
|
||||
}
|
||||
|
||||
if partsLength != len(rawQuery) {
|
||||
t.Errorf("(%d) Expected %d characters, got %d in \n%v", i, partsLength, len(rawQuery), rawQuery)
|
||||
if !list.ExistInSliceWithRegex(rawQuery, []string{s.expectQuery}) {
|
||||
t.Errorf("(%d) Expected query\n %v \ngot:\n %v", i, s.expectQuery, rawQuery)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,7 +130,16 @@ func TestRecordFieldResolverResolveSchemaFields(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, nil)
|
||||
requestData := map[string]any{
|
||||
"user": map[string]any{
|
||||
"id": "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c",
|
||||
"profile": map[string]any{
|
||||
"id": "d13f60a4-5765-48c7-9e1d-3e782340f833",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData)
|
||||
|
||||
scenarios := []struct {
|
||||
fieldName string
|
||||
@@ -118,42 +160,45 @@ func TestRecordFieldResolverResolveSchemaFields(t *testing.T) {
|
||||
{"manyrels.unknown", true, ""},
|
||||
{"manyrels.title", false, "[[demo4_manyrels.title]]"},
|
||||
{"manyrels.onerel.manyrels.onefile", false, "[[demo4_manyrels_onerel_manyrels.onefile]]"},
|
||||
// @request.user.profile relation join:
|
||||
{"@request.user.profile.rel.name", false, "[[__user_profiles_rel.name]]"},
|
||||
// @collection fieds:
|
||||
{"@collect", true, ""},
|
||||
{"collection.demo4.title", true, ""},
|
||||
{"@collection", true, ""},
|
||||
{"@collection.unknown", true, ""},
|
||||
{"@collection.demo", true, ""},
|
||||
{"@collection.demo.", true, ""},
|
||||
{"@collection.demo.title", false, "[[c_demo.title]]"},
|
||||
{"@collection.demo4.title", false, "[[c_demo4.title]]"},
|
||||
{"@collection.demo4.id", false, "[[c_demo4.id]]"},
|
||||
{"@collection.demo4.created", false, "[[c_demo4.created]]"},
|
||||
{"@collection.demo4.updated", false, "[[c_demo4.updated]]"},
|
||||
{"@collection.demo.title", false, "[[__collection_demo.title]]"},
|
||||
{"@collection.demo4.title", false, "[[__collection_demo4.title]]"},
|
||||
{"@collection.demo4.id", false, "[[__collection_demo4.id]]"},
|
||||
{"@collection.demo4.created", false, "[[__collection_demo4.created]]"},
|
||||
{"@collection.demo4.updated", false, "[[__collection_demo4.updated]]"},
|
||||
{"@collection.demo4.manyrels.missing", true, ""},
|
||||
{"@collection.demo4.manyrels.onerel.manyrels.onerel.onefile", false, "[[c_demo4_manyrels_onerel_manyrels_onerel.onefile]]"},
|
||||
{"@collection.demo4.manyrels.onerel.manyrels.onerel.onefile", false, "[[__collection_demo4_manyrels_onerel_manyrels_onerel.onefile]]"},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
for _, s := range scenarios {
|
||||
name, params, err := r.Resolve(s.fieldName)
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err)
|
||||
t.Errorf("(%q) Expected hasErr %v, got %v (%v)", s.fieldName, s.expectError, hasErr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if name != s.expectName {
|
||||
t.Errorf("(%d) Expected name %q, got %q", i, s.expectName, name)
|
||||
t.Errorf("(%q) Expected name %q, got %q", s.fieldName, s.expectName, name)
|
||||
}
|
||||
|
||||
// params should be empty for non @request fields
|
||||
if len(params) != 0 {
|
||||
t.Errorf("(%d) Expected 0 params, got %v", i, params)
|
||||
t.Errorf("(%q) Expected 0 params, got %v", s.fieldName, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordFieldResolverResolveRequestDataFields(t *testing.T) {
|
||||
func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
@@ -171,7 +216,13 @@ func TestRecordFieldResolverResolveRequestDataFields(t *testing.T) {
|
||||
"b": 456,
|
||||
"c": map[string]int{"sub": 1},
|
||||
},
|
||||
"user": nil,
|
||||
"user": map[string]any{
|
||||
"id": "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c",
|
||||
"profile": map[string]any{
|
||||
"id": "d13f60a4-5765-48c7-9e1d-3e782340f833",
|
||||
"name": "test",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData)
|
||||
@@ -194,7 +245,9 @@ func TestRecordFieldResolverResolveRequestDataFields(t *testing.T) {
|
||||
{"@request.data.b.missing", false, ``},
|
||||
{"@request.data.c", false, `"{\"sub\":1}"`},
|
||||
{"@request.user", true, ""},
|
||||
{"@request.user.id", false, ""},
|
||||
{"@request.user.id", false, `"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`},
|
||||
{"@request.user.profile", false, `"{\"id\":\"d13f60a4-5765-48c7-9e1d-3e782340f833\",\"name\":\"test\"}"`},
|
||||
{"@request.user.profile.name", false, `"test"`},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
|
||||
+27
-7
@@ -22,11 +22,20 @@ type ApiScenario struct {
|
||||
Url string
|
||||
Body io.Reader
|
||||
RequestHeaders map[string]string
|
||||
|
||||
// Delay adds a delay before checking the expectations usually
|
||||
// to ensure that all fired non-awaited go routines have finished
|
||||
Delay time.Duration
|
||||
|
||||
// expectations
|
||||
ExpectedStatus int
|
||||
ExpectedContent []string
|
||||
ExpectedEvents map[string]int
|
||||
// test events
|
||||
// ---
|
||||
ExpectedStatus int
|
||||
ExpectedContent []string
|
||||
NotExpectedContent []string
|
||||
ExpectedEvents map[string]int
|
||||
|
||||
// test hooks
|
||||
// ---
|
||||
BeforeFunc func(t *testing.T, app *TestApp, e *echo.Echo)
|
||||
AfterFunc func(t *testing.T, app *TestApp, e *echo.Echo)
|
||||
}
|
||||
@@ -80,9 +89,13 @@ func (scenario *ApiScenario) Test(t *testing.T) {
|
||||
t.Errorf("[%s] Expected status code %d, got %d", prefix, scenario.ExpectedStatus, res.StatusCode)
|
||||
}
|
||||
|
||||
if len(scenario.ExpectedContent) == 0 {
|
||||
if scenario.Delay > 0 {
|
||||
time.Sleep(scenario.Delay)
|
||||
}
|
||||
|
||||
if len(scenario.ExpectedContent) == 0 && len(scenario.NotExpectedContent) == 0 {
|
||||
if len(recorder.Body.Bytes()) != 0 {
|
||||
t.Errorf("[%s] Expected empty body, got %v", prefix, recorder.Body.String())
|
||||
t.Errorf("[%s] Expected empty body, got \n%v", prefix, recorder.Body.String())
|
||||
}
|
||||
} else {
|
||||
// normalize json response format
|
||||
@@ -98,7 +111,14 @@ func (scenario *ApiScenario) Test(t *testing.T) {
|
||||
|
||||
for _, item := range scenario.ExpectedContent {
|
||||
if !strings.Contains(normalizedBody, item) {
|
||||
t.Errorf("[%s] Cannot find %v in response body %v", prefix, item, normalizedBody)
|
||||
t.Errorf("[%s] Cannot find %v in response body \n%v", prefix, item, normalizedBody)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range scenario.NotExpectedContent {
|
||||
if strings.Contains(normalizedBody, item) {
|
||||
t.Errorf("[%s] Didn't expect %v in response body \n%v", prefix, item, normalizedBody)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
"gocloud.dev/blob"
|
||||
"gocloud.dev/blob/fileblob"
|
||||
"gocloud.dev/blob/s3blob"
|
||||
@@ -35,15 +36,17 @@ func NewS3(
|
||||
endpoint string,
|
||||
accessKey string,
|
||||
secretKey string,
|
||||
s3ForcePathStyle bool,
|
||||
) (*System, error) {
|
||||
ctx := context.Background() // default context
|
||||
|
||||
cred := credentials.NewStaticCredentials(accessKey, secretKey, "")
|
||||
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(region),
|
||||
Endpoint: aws.String(endpoint),
|
||||
Credentials: cred,
|
||||
Region: aws.String(region),
|
||||
Endpoint: aws.String(endpoint),
|
||||
Credentials: cred,
|
||||
S3ForcePathStyle: aws.Bool(s3ForcePathStyle),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -174,6 +177,11 @@ func (s *System) DeletePrefix(prefix string) []error {
|
||||
return failed
|
||||
}
|
||||
|
||||
var inlineServeContentTypes = []string{
|
||||
"image/png", "image/jpg", "image/jpeg", "image/gif",
|
||||
"video/mp4", "video/3gpp", "video/quicktime", " video/x-ms-wmv",
|
||||
}
|
||||
|
||||
// Serve serves the file at fileKey location to an HTTP response.
|
||||
func (s *System) Serve(response http.ResponseWriter, fileKey string, name string) error {
|
||||
r, readErr := s.bucket.NewReader(s.ctx, fileKey, nil)
|
||||
@@ -182,9 +190,24 @@ func (s *System) Serve(response http.ResponseWriter, fileKey string, name string
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
response.Header().Set("Content-Disposition", "attachment; filename="+name)
|
||||
response.Header().Set("Content-Type", r.ContentType())
|
||||
disposition := "attachment"
|
||||
realContentType := r.ContentType()
|
||||
if list.ExistInSlice(realContentType, inlineServeContentTypes) {
|
||||
disposition = "inline"
|
||||
}
|
||||
|
||||
// make an exception for svg and use a custom content type
|
||||
// to send in the response so that it can be loaded in a img tag
|
||||
// (see https://github.com/whatwg/mimesniff/issues/7)
|
||||
extContentType := realContentType
|
||||
if filepath.Ext(name) == ".svg" {
|
||||
extContentType = "image/svg+xml"
|
||||
}
|
||||
|
||||
response.Header().Set("Content-Disposition", disposition+"; filename="+name)
|
||||
response.Header().Set("Content-Type", extContentType)
|
||||
response.Header().Set("Content-Length", strconv.FormatInt(r.Size(), 10))
|
||||
response.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
|
||||
|
||||
// All HTTP date/time stamps MUST be represented in Greenwich Mean Time (GMT)
|
||||
// (see https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1)
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestFileSystemExists(t *testing.T) {
|
||||
{"sub1.txt", false},
|
||||
{"test/sub1.txt", true},
|
||||
{"test/sub2.txt", true},
|
||||
{"file.png", true},
|
||||
{"image.png", true},
|
||||
}
|
||||
|
||||
for i, scenario := range scenarios {
|
||||
@@ -51,13 +51,14 @@ func TestFileSystemAttributes(t *testing.T) {
|
||||
defer fs.Close()
|
||||
|
||||
scenarios := []struct {
|
||||
file string
|
||||
expectError bool
|
||||
file string
|
||||
expectError bool
|
||||
expectContentType string
|
||||
}{
|
||||
{"sub1.txt", true},
|
||||
{"test/sub1.txt", false},
|
||||
{"test/sub2.txt", false},
|
||||
{"file.png", false},
|
||||
{"sub1.txt", true, ""},
|
||||
{"test/sub1.txt", false, "application/octet-stream"},
|
||||
{"test/sub2.txt", false, "application/octet-stream"},
|
||||
{"image.png", false, "image/png"},
|
||||
}
|
||||
|
||||
for i, scenario := range scenarios {
|
||||
@@ -71,8 +72,8 @@ func TestFileSystemAttributes(t *testing.T) {
|
||||
t.Errorf("(%d) Expected nil, got error, %v", i, err)
|
||||
}
|
||||
|
||||
if err == nil && attr.ContentType != "application/octet-stream" {
|
||||
t.Errorf("(%d) Expected attr.ContentType to be %q, got %q", i, "application/octet-stream", attr.ContentType)
|
||||
if err == nil && attr.ContentType != scenario.expectContentType {
|
||||
t.Errorf("(%d) Expected attr.ContentType to be %q, got %q", i, scenario.expectContentType, attr.ContentType)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,7 +92,7 @@ func TestFileSystemDelete(t *testing.T) {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
|
||||
if err := fs.Delete("file.png"); err != nil {
|
||||
if err := fs.Delete("image.png"); err != nil {
|
||||
t.Fatalf("Expected nil, got error %v", err)
|
||||
}
|
||||
}
|
||||
@@ -157,33 +158,73 @@ func TestFileSystemServe(t *testing.T) {
|
||||
}
|
||||
defer fs.Close()
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
|
||||
// serve missing file
|
||||
if err := fs.Serve(r, "missing.txt", "download.txt"); err == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
|
||||
// serve existing file
|
||||
if err := fs.Serve(r, "test/sub1.txt", "download.txt"); err != nil {
|
||||
t.Fatal("Expected nil, got error")
|
||||
}
|
||||
|
||||
result := r.Result()
|
||||
|
||||
// check headers
|
||||
scenarios := []struct {
|
||||
header string
|
||||
expected string
|
||||
path string
|
||||
name string
|
||||
expectError bool
|
||||
expectHeaders map[string]string
|
||||
}{
|
||||
{"Content-Disposition", "attachment; filename=download.txt"},
|
||||
{"Content-Type", "application/octet-stream"},
|
||||
{"Content-Length", "0"},
|
||||
{
|
||||
// missing
|
||||
"missing.txt",
|
||||
"test_name.txt",
|
||||
true,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
// existing regular file
|
||||
"test/sub1.txt",
|
||||
"test_name.txt",
|
||||
false,
|
||||
map[string]string{
|
||||
"Content-Disposition": "attachment; filename=test_name.txt",
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Length": "0",
|
||||
},
|
||||
},
|
||||
// png inline
|
||||
{
|
||||
// svg exception
|
||||
"image.png",
|
||||
"test_name.png",
|
||||
false,
|
||||
map[string]string{
|
||||
"Content-Disposition": "inline; filename=test_name.png",
|
||||
"Content-Type": "image/png",
|
||||
"Content-Length": "73",
|
||||
},
|
||||
},
|
||||
{
|
||||
// svg exception
|
||||
"image.svg",
|
||||
"test_name.svg",
|
||||
false,
|
||||
map[string]string{
|
||||
"Content-Disposition": "attachment; filename=test_name.svg",
|
||||
"Content-Type": "image/svg+xml",
|
||||
"Content-Length": "0",
|
||||
},
|
||||
},
|
||||
}
|
||||
for i, scenario := range scenarios {
|
||||
v := result.Header.Get(scenario.header)
|
||||
if v != scenario.expected {
|
||||
t.Errorf("(%d) Expected value %q for header %q, got %q", i, scenario.expected, scenario.header, v)
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
r := httptest.NewRecorder()
|
||||
|
||||
err := fs.Serve(r, scenario.path, scenario.name)
|
||||
hasErr := err != nil
|
||||
|
||||
if hasErr != scenario.expectError {
|
||||
t.Errorf("(%s) Expected hasError %v, got %v", scenario.path, scenario.expectError, hasErr)
|
||||
continue
|
||||
}
|
||||
|
||||
result := r.Result()
|
||||
|
||||
for hName, hValue := range scenario.expectHeaders {
|
||||
v := result.Header.Get(hName)
|
||||
if v != hValue {
|
||||
t.Errorf("(%s) Expected value %q for header %q, got %q", scenario.path, hValue, hName, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,11 +250,11 @@ func TestFileSystemCreateThumb(t *testing.T) {
|
||||
// non-image existing file
|
||||
{"test/sub1.txt", "thumb_test_sub1", true, true},
|
||||
// existing image file - crop center
|
||||
{"file.png", "thumb_file_center", true, false},
|
||||
{"image.png", "thumb_file_center", true, false},
|
||||
// existing image file - crop top
|
||||
{"file.png", "thumb_file_top", false, false},
|
||||
{"image.png", "thumb_file_top", false, false},
|
||||
// existing image file with existing thumb path = should fail
|
||||
{"file.png", "test", true, true},
|
||||
{"image.png", "test", true, true},
|
||||
}
|
||||
|
||||
for i, scenario := range scenarios {
|
||||
@@ -259,7 +300,7 @@ func createTestDir(t *testing.T) string {
|
||||
}
|
||||
file2.Close()
|
||||
|
||||
file3, err := os.OpenFile(filepath.Join(dir, "file.png"), os.O_WRONLY|os.O_CREATE, 0666)
|
||||
file3, err := os.OpenFile(filepath.Join(dir, "image.png"), os.O_WRONLY|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -267,6 +308,16 @@ func createTestDir(t *testing.T) string {
|
||||
imgRect := image.Rect(0, 0, 1, 1)
|
||||
png.Encode(file3, imgRect)
|
||||
file3.Close()
|
||||
err2 := os.WriteFile(filepath.Join(dir, "image.png.attrs"), []byte(`{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null}`), 0666)
|
||||
if err2 != nil {
|
||||
t.Fatal(err2)
|
||||
}
|
||||
|
||||
file4, err := os.OpenFile(filepath.Join(dir, "image.svg"), os.O_WRONLY|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file4.Close()
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
@@ -194,11 +194,9 @@ func (r *Runner) Up() ([]string, error) {
|
||||
//
|
||||
// On success returns list with the reverted migrations file names.
|
||||
func (r *Runner) Down(toRevertCount int) ([]string, error) {
|
||||
applied := []string{}
|
||||
reverted := make([]string, 0, toRevertCount)
|
||||
|
||||
err := r.db.Transactional(func(tx *dbx.Tx) error {
|
||||
totalReverted := 0
|
||||
|
||||
for i := len(r.migrationsList.Items()) - 1; i >= 0; i-- {
|
||||
m := r.migrationsList.Item(i)
|
||||
|
||||
@@ -208,7 +206,7 @@ func (r *Runner) Down(toRevertCount int) ([]string, error) {
|
||||
}
|
||||
|
||||
// revert limit reached
|
||||
if toRevertCount-totalReverted <= 0 {
|
||||
if toRevertCount-len(reverted) <= 0 {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -220,7 +218,7 @@ func (r *Runner) Down(toRevertCount int) ([]string, error) {
|
||||
return fmt.Errorf("Failed to save reverted migration info for %s: %w", m.file, err)
|
||||
}
|
||||
|
||||
applied = append(applied, m.file)
|
||||
reverted = append(reverted, m.file)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -229,7 +227,7 @@ func (r *Runner) Down(toRevertCount int) ([]string, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return applied, nil
|
||||
return reverted, nil
|
||||
}
|
||||
|
||||
func (r *Runner) createMigrationsTable() error {
|
||||
|
||||
@@ -104,7 +104,8 @@ func TestRunnerUpAndDown(t *testing.T) {
|
||||
|
||||
// Down()
|
||||
// ---
|
||||
if _, err := r.Down(2); err != nil {
|
||||
// revert one migration
|
||||
if _, err := r.Down(1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -112,8 +113,12 @@ func TestRunnerUpAndDown(t *testing.T) {
|
||||
t.Fatal("Didn't expect 3_test to be reverted.")
|
||||
}
|
||||
|
||||
if !test1DownCalled || !test2DownCalled {
|
||||
t.Fatalf("Expected 1_test and 2_test to be reverted, got %v and %v", test1DownCalled, test2DownCalled)
|
||||
if !test2DownCalled {
|
||||
t.Fatal("Expected 2_test to be reverted.")
|
||||
}
|
||||
|
||||
if test1DownCalled {
|
||||
t.Fatal("Didn't expect 1_test to be reverted.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
@@ -15,6 +16,8 @@ import (
|
||||
// will be used when parsing a form request body.
|
||||
const DefaultMaxMemory = 32 << 20 // 32mb
|
||||
|
||||
var extensionInvalidCharsRegex = regexp.MustCompile(`[^\w\.\*\-\+\=\#]+`)
|
||||
|
||||
// UploadedFile defines a single multipart uploaded file instance.
|
||||
type UploadedFile struct {
|
||||
name string
|
||||
@@ -65,8 +68,10 @@ func FindUploadedFiles(r *http.Request, key string) ([]*UploadedFile, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ext := extensionInvalidCharsRegex.ReplaceAllString(filepath.Ext(fh.Filename), "")
|
||||
|
||||
result[i] = &UploadedFile{
|
||||
name: fmt.Sprintf("%s%s", security.RandomString(32), filepath.Ext(fh.Filename)),
|
||||
name: fmt.Sprintf("%s%s", security.RandomString(32), ext),
|
||||
header: fh,
|
||||
bytes: buf.Bytes(),
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ type Provider struct {
|
||||
query *dbx.SelectQuery
|
||||
page int
|
||||
perPage int
|
||||
countColumn string
|
||||
sort []SortField
|
||||
filter []FilterData
|
||||
}
|
||||
@@ -67,6 +68,13 @@ func (s *Provider) Query(query *dbx.SelectQuery) *Provider {
|
||||
return s
|
||||
}
|
||||
|
||||
// CountColumn specifies an optional distinct column to use in the
|
||||
// SELECT COUNT query.
|
||||
func (s *Provider) CountColumn(countColumn string) *Provider {
|
||||
s.countColumn = countColumn
|
||||
return s
|
||||
}
|
||||
|
||||
// Page sets the `page` field of the current search provider.
|
||||
//
|
||||
// Normalization on the `page` value is done during `Exec()`.
|
||||
@@ -190,7 +198,11 @@ func (s *Provider) Exec(items any) (*Result, error) {
|
||||
// count
|
||||
var totalCount int64
|
||||
countQuery := modelsQuery
|
||||
if err := countQuery.Select("count(*)").Row(&totalCount); err != nil {
|
||||
countQuery.Distinct(false).Select("COUNT(*)")
|
||||
if s.countColumn != "" {
|
||||
countQuery.Select("COUNT(DISTINCT(" + s.countColumn + "))")
|
||||
}
|
||||
if err := countQuery.Row(&totalCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,15 @@ func TestProviderPerPage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderCountColumn(t *testing.T) {
|
||||
r := &testFieldResolver{}
|
||||
p := NewProvider(r).CountColumn("test")
|
||||
|
||||
if p.countColumn != "test" {
|
||||
t.Fatalf("Expected distinct count column %v, got %v", "test", p.countColumn)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderSort(t *testing.T) {
|
||||
initialSort := []SortField{{"test1", SortAsc}, {"test2", SortAsc}}
|
||||
r := &testFieldResolver{}
|
||||
@@ -214,6 +223,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
|
||||
perPage int
|
||||
sort []SortField
|
||||
filter []FilterData
|
||||
countColumn string
|
||||
expectError bool
|
||||
expectResult string
|
||||
expectQueries []string
|
||||
@@ -224,10 +234,11 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
|
||||
10,
|
||||
[]SortField{},
|
||||
[]FilterData{},
|
||||
"",
|
||||
false,
|
||||
`{"page":1,"perPage":10,"totalItems":2,"items":[{"test1":1,"test2":"test2.1","test3":""},{"test1":2,"test2":"test2.2","test3":""}]}`,
|
||||
[]string{
|
||||
"SELECT count(*) FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC",
|
||||
"SELECT COUNT(*) FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC",
|
||||
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 10",
|
||||
},
|
||||
},
|
||||
@@ -237,10 +248,11 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
|
||||
0, // fallback to default
|
||||
[]SortField{},
|
||||
[]FilterData{},
|
||||
"",
|
||||
false,
|
||||
`{"page":1,"perPage":30,"totalItems":2,"items":[{"test1":1,"test2":"test2.1","test3":""},{"test1":2,"test2":"test2.2","test3":""}]}`,
|
||||
[]string{
|
||||
"SELECT count(*) FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC",
|
||||
"SELECT COUNT(*) FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC",
|
||||
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 30",
|
||||
},
|
||||
},
|
||||
@@ -250,6 +262,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
|
||||
10,
|
||||
[]SortField{{"unknown", SortAsc}},
|
||||
[]FilterData{},
|
||||
"",
|
||||
true,
|
||||
"",
|
||||
nil,
|
||||
@@ -260,6 +273,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
|
||||
10,
|
||||
[]SortField{},
|
||||
[]FilterData{"test2 = 'test2.1'", "invalid"},
|
||||
"",
|
||||
true,
|
||||
"",
|
||||
nil,
|
||||
@@ -270,10 +284,11 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
|
||||
5555, // will be limited by MaxPerPage
|
||||
[]SortField{{"test2", SortDesc}},
|
||||
[]FilterData{"test2 != null", "test1 >= 2"},
|
||||
"",
|
||||
false,
|
||||
`{"page":1,"perPage":` + fmt.Sprint(MaxPerPage) + `,"totalItems":1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
|
||||
[]string{
|
||||
"SELECT count(*) FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= '2') ORDER BY `test1` ASC, `test2` DESC",
|
||||
"SELECT COUNT(*) FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= '2') ORDER BY `test1` ASC, `test2` DESC",
|
||||
"SELECT * FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= '2') ORDER BY `test1` ASC, `test2` DESC LIMIT 200",
|
||||
},
|
||||
},
|
||||
@@ -283,10 +298,11 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
|
||||
10,
|
||||
[]SortField{{"test3", SortAsc}},
|
||||
[]FilterData{"test3 != ''"},
|
||||
"",
|
||||
false,
|
||||
`{"page":1,"perPage":10,"totalItems":0,"items":[]}`,
|
||||
[]string{
|
||||
"SELECT count(*) FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', '')) ORDER BY `test1` ASC, `test3` ASC",
|
||||
"SELECT COUNT(*) FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', '')) ORDER BY `test1` ASC, `test3` ASC",
|
||||
"SELECT * FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', '')) ORDER BY `test1` ASC, `test3` ASC LIMIT 10",
|
||||
},
|
||||
},
|
||||
@@ -296,10 +312,25 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
|
||||
1,
|
||||
[]SortField{},
|
||||
[]FilterData{},
|
||||
"",
|
||||
false,
|
||||
`{"page":2,"perPage":1,"totalItems":2,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
|
||||
[]string{
|
||||
"SELECT count(*) FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC",
|
||||
"SELECT COUNT(*) FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC",
|
||||
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 1 OFFSET 1",
|
||||
},
|
||||
},
|
||||
// distinct count column
|
||||
{
|
||||
3,
|
||||
1,
|
||||
[]SortField{},
|
||||
[]FilterData{},
|
||||
"test.test1",
|
||||
false,
|
||||
`{"page":2,"perPage":1,"totalItems":2,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
|
||||
[]string{
|
||||
"SELECT COUNT(DISTINCT(test.test1)) FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC",
|
||||
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 1 OFFSET 1",
|
||||
},
|
||||
},
|
||||
@@ -314,7 +345,8 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
|
||||
Page(s.page).
|
||||
PerPage(s.perPage).
|
||||
Sort(s.sort).
|
||||
Filter(s.filter)
|
||||
Filter(s.filter).
|
||||
CountColumn(s.countColumn)
|
||||
|
||||
result, err := p.Exec(&[]testTableStruct{})
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,2 +1,2 @@
|
||||
import{S as E,i as G,s as I,F as K,c as F,m as B,t as H,a as N,d as T,C as M,q as J,e as c,w as q,b as k,f as u,r as L,g as b,h as _,u as h,v as O,j as Q,l as U,o as w,A as V,p as W,B as X,D as Y,x as Z,z as S}from"./index.0d48f812.js";function y(f){let e,o,s;return{c(){e=q("for "),o=c("strong"),s=q(f[3]),u(o,"class","txt-nowrap")},m(l,t){b(l,e,t),b(l,o,t),_(o,s)},p(l,t){t&8&&Z(s,l[3])},d(l){l&&w(e),l&&w(o)}}}function x(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password"),l=k(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0,t.autofocus=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[0]),t.focus(),p||(d=h(t,"input",f[6]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&1&&t.value!==n[0]&&S(t,n[0])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function ee(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password confirm"),l=k(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[1]),p||(d=h(t,"input",f[7]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&2&&t.value!==n[1]&&S(t,n[1])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function te(f){let e,o,s,l,t,r,p,d,n,i,g,R,C,v,P,A,j,m=f[3]&&y(f);return r=new J({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),d=new J({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[ee,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),{c(){e=c("form"),o=c("div"),s=c("h4"),l=q(`Reset your admin password
|
||||
import{S as E,i as G,s as I,F as K,c as F,m as B,t as H,a as N,d as T,C as M,q as J,e as c,w as q,b as k,f as u,r as L,g as b,h as _,u as h,v as O,j as Q,l as U,o as w,A as V,p as W,B as X,D as Y,x as Z,z as S}from"./index.38300823.js";function y(f){let e,o,s;return{c(){e=q("for "),o=c("strong"),s=q(f[3]),u(o,"class","txt-nowrap")},m(l,t){b(l,e,t),b(l,o,t),_(o,s)},p(l,t){t&8&&Z(s,l[3])},d(l){l&&w(e),l&&w(o)}}}function x(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password"),l=k(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0,t.autofocus=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[0]),t.focus(),p||(d=h(t,"input",f[6]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&1&&t.value!==n[0]&&S(t,n[0])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function ee(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password confirm"),l=k(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[1]),p||(d=h(t,"input",f[7]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&2&&t.value!==n[1]&&S(t,n[1])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function te(f){let e,o,s,l,t,r,p,d,n,i,g,R,C,v,P,A,j,m=f[3]&&y(f);return r=new J({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),d=new J({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[ee,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),{c(){e=c("form"),o=c("div"),s=c("h4"),l=q(`Reset your admin password
|
||||
`),m&&m.c(),t=k(),F(r.$$.fragment),p=k(),F(d.$$.fragment),n=k(),i=c("button"),g=c("span"),g.textContent="Set new password",R=k(),C=c("div"),v=c("a"),v.textContent="Back to login",u(s,"class","m-b-xs"),u(o,"class","content txt-center m-b-sm"),u(g,"class","txt"),u(i,"type","submit"),u(i,"class","btn btn-lg btn-block"),i.disabled=f[2],L(i,"btn-loading",f[2]),u(e,"class","m-b-base"),u(v,"href","/login"),u(v,"class","link-hint"),u(C,"class","content txt-center")},m(a,$){b(a,e,$),_(e,o),_(o,s),_(s,l),m&&m.m(s,null),_(e,t),B(r,e,null),_(e,p),B(d,e,null),_(e,n),_(e,i),_(i,g),b(a,R,$),b(a,C,$),_(C,v),P=!0,A||(j=[h(e,"submit",O(f[4])),Q(U.call(null,v))],A=!0)},p(a,$){a[3]?m?m.p(a,$):(m=y(a),m.c(),m.m(s,null)):m&&(m.d(1),m=null);const z={};$&769&&(z.$$scope={dirty:$,ctx:a}),r.$set(z);const D={};$&770&&(D.$$scope={dirty:$,ctx:a}),d.$set(D),(!P||$&4)&&(i.disabled=a[2]),$&4&&L(i,"btn-loading",a[2])},i(a){P||(H(r.$$.fragment,a),H(d.$$.fragment,a),P=!0)},o(a){N(r.$$.fragment,a),N(d.$$.fragment,a),P=!1},d(a){a&&w(e),m&&m.d(),T(r),T(d),a&&w(R),a&&w(C),A=!1,V(j)}}}function se(f){let e,o;return e=new K({props:{$$slots:{default:[te]},$$scope:{ctx:f}}}),{c(){F(e.$$.fragment)},m(s,l){B(e,s,l),o=!0},p(s,[l]){const t={};l&527&&(t.$$scope={dirty:l,ctx:s}),e.$set(t)},i(s){o||(H(e.$$.fragment,s),o=!0)},o(s){N(e.$$.fragment,s),o=!1},d(s){T(e,s)}}}function le(f,e,o){let s,{params:l}=e,t="",r="",p=!1;async function d(){if(!p){o(2,p=!0);try{await W.Admins.confirmPasswordReset(l==null?void 0:l.token,t,r),X("Successfully set a new admin password."),Y("/")}catch(g){W.errorResponseHandler(g)}o(2,p=!1)}}function n(){t=this.value,o(0,t)}function i(){r=this.value,o(1,r)}return f.$$set=g=>{"params"in g&&o(5,l=g.params)},f.$$.update=()=>{f.$$.dirty&32&&o(3,s=M.getJWTPayload(l==null?void 0:l.token).email||"")},[t,r,p,s,d,l,n,i]}class ae extends E{constructor(e){super(),G(this,e,le,se,I,{params:5})}}export{ae as default};
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
import{S as E,i as M,s as T,F as j,c as H,m as L,t as w,a as y,d as S,b as g,e as _,f as p,g as k,h as d,j as z,l as B,k as N,n as D,o as v,p as C,q as G,r as F,u as A,v as I,w as h,x as J,y as P,z as R}from"./index.0d48f812.js";function K(c){let e,s,n,l,t,o,f,m,i,a,b,u;return l=new G({props:{class:"form-field required",name:"email",$$slots:{default:[Q,({uniqueId:r})=>({5:r}),({uniqueId:r})=>r?32:0]},$$scope:{ctx:c}}}),{c(){e=_("form"),s=_("div"),s.innerHTML=`<h4 class="m-b-xs">Forgotten admin password</h4>
|
||||
import{S as E,i as M,s as T,F as j,c as H,m as L,t as w,a as y,d as S,b as g,e as _,f as p,g as k,h as d,j as z,l as B,k as N,n as D,o as v,p as C,q as G,r as F,u as A,v as I,w as h,x as J,y as P,z as R}from"./index.38300823.js";function K(c){let e,s,n,l,t,o,f,m,i,a,b,u;return l=new G({props:{class:"form-field required",name:"email",$$slots:{default:[Q,({uniqueId:r})=>({5:r}),({uniqueId:r})=>r?32:0]},$$scope:{ctx:c}}}),{c(){e=_("form"),s=_("div"),s.innerHTML=`<h4 class="m-b-xs">Forgotten admin password</h4>
|
||||
<p>Enter the email associated with your account and we\u2019ll send you a recovery link:</p>`,n=g(),H(l.$$.fragment),t=g(),o=_("button"),f=_("i"),m=g(),i=_("span"),i.textContent="Send recovery link",p(s,"class","content txt-center m-b-sm"),p(f,"class","ri-mail-send-line"),p(i,"class","txt"),p(o,"type","submit"),p(o,"class","btn btn-lg btn-block"),o.disabled=c[1],F(o,"btn-loading",c[1]),p(e,"class","m-b-base")},m(r,$){k(r,e,$),d(e,s),d(e,n),L(l,e,null),d(e,t),d(e,o),d(o,f),d(o,m),d(o,i),a=!0,b||(u=A(e,"submit",I(c[3])),b=!0)},p(r,$){const q={};$&97&&(q.$$scope={dirty:$,ctx:r}),l.$set(q),(!a||$&2)&&(o.disabled=r[1]),$&2&&F(o,"btn-loading",r[1])},i(r){a||(w(l.$$.fragment,r),a=!0)},o(r){y(l.$$.fragment,r),a=!1},d(r){r&&v(e),S(l),b=!1,u()}}}function O(c){let e,s,n,l,t,o,f,m,i;return{c(){e=_("div"),s=_("div"),s.innerHTML='<i class="ri-checkbox-circle-line"></i>',n=g(),l=_("div"),t=_("p"),o=h("Check "),f=_("strong"),m=h(c[0]),i=h(" for the recovery link."),p(s,"class","icon"),p(f,"class","txt-nowrap"),p(l,"class","content"),p(e,"class","alert alert-success")},m(a,b){k(a,e,b),d(e,s),d(e,n),d(e,l),d(l,t),d(t,o),d(t,f),d(f,m),d(t,i)},p(a,b){b&1&&J(m,a[0])},i:P,o:P,d(a){a&&v(e)}}}function Q(c){let e,s,n,l,t,o,f,m;return{c(){e=_("label"),s=h("Email"),l=g(),t=_("input"),p(e,"for",n=c[5]),p(t,"type","email"),p(t,"id",o=c[5]),t.required=!0,t.autofocus=!0},m(i,a){k(i,e,a),d(e,s),k(i,l,a),k(i,t,a),R(t,c[0]),t.focus(),f||(m=A(t,"input",c[4]),f=!0)},p(i,a){a&32&&n!==(n=i[5])&&p(e,"for",n),a&32&&o!==(o=i[5])&&p(t,"id",o),a&1&&t.value!==i[0]&&R(t,i[0])},d(i){i&&v(e),i&&v(l),i&&v(t),f=!1,m()}}}function U(c){let e,s,n,l,t,o,f,m;const i=[O,K],a=[];function b(u,r){return u[2]?0:1}return e=b(c),s=a[e]=i[e](c),{c(){s.c(),n=g(),l=_("div"),t=_("a"),t.textContent="Back to login",p(t,"href","/login"),p(t,"class","link-hint"),p(l,"class","content txt-center")},m(u,r){a[e].m(u,r),k(u,n,r),k(u,l,r),d(l,t),o=!0,f||(m=z(B.call(null,t)),f=!0)},p(u,r){let $=e;e=b(u),e===$?a[e].p(u,r):(N(),y(a[$],1,1,()=>{a[$]=null}),D(),s=a[e],s?s.p(u,r):(s=a[e]=i[e](u),s.c()),w(s,1),s.m(n.parentNode,n))},i(u){o||(w(s),o=!0)},o(u){y(s),o=!1},d(u){a[e].d(u),u&&v(n),u&&v(l),f=!1,m()}}}function V(c){let e,s;return e=new j({props:{$$slots:{default:[U]},$$scope:{ctx:c}}}),{c(){H(e.$$.fragment)},m(n,l){L(e,n,l),s=!0},p(n,[l]){const t={};l&71&&(t.$$scope={dirty:l,ctx:n}),e.$set(t)},i(n){s||(w(e.$$.fragment,n),s=!0)},o(n){y(e.$$.fragment,n),s=!1},d(n){S(e,n)}}}function W(c,e,s){let n="",l=!1,t=!1;async function o(){if(!l){s(1,l=!0);try{await C.Admins.requestPasswordReset(n),s(2,t=!0)}catch(m){C.errorResponseHandler(m)}s(1,l=!1)}}function f(){n=this.value,s(0,n)}return[n,l,t,o,f]}class Y extends E{constructor(e){super(),M(this,e,W,V,T,{})}}export{Y as default};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as J,i as M,s as N,F as R,c as S,m as U,t as $,a as v,d as z,C as W,E as Y,g as _,k as j,n as A,o as b,p as F,q as B,e as m,w as y,b as C,f as d,r as H,h as k,u as E,v as D,y as h,x as G,z as T}from"./index.0d48f812.js";function I(r){let e,s,t,l,n,o,c,a,i,u,g,q,p=r[3]&&L(r);return o=new B({props:{class:"form-field required",name:"password",$$slots:{default:[O,({uniqueId:f})=>({8:f}),({uniqueId:f})=>f?256:0]},$$scope:{ctx:r}}}),{c(){e=m("form"),s=m("div"),t=m("h4"),l=y(`Type your password to confirm changing your email address
|
||||
import{S as J,i as M,s as N,F as R,c as S,m as U,t as $,a as v,d as z,C as W,E as Y,g as _,k as j,n as A,o as b,p as F,q as B,e as m,w as y,b as C,f as d,r as H,h as k,u as E,v as D,y as h,x as G,z as T}from"./index.38300823.js";function I(r){let e,s,t,l,n,o,c,a,i,u,g,q,p=r[3]&&L(r);return o=new B({props:{class:"form-field required",name:"password",$$slots:{default:[O,({uniqueId:f})=>({8:f}),({uniqueId:f})=>f?256:0]},$$scope:{ctx:r}}}),{c(){e=m("form"),s=m("div"),t=m("h4"),l=y(`Type your password to confirm changing your email address
|
||||
`),p&&p.c(),n=C(),S(o.$$.fragment),c=C(),a=m("button"),i=m("span"),i.textContent="Confirm new email",d(t,"class","m-b-xs"),d(s,"class","content txt-center m-b-sm"),d(i,"class","txt"),d(a,"type","submit"),d(a,"class","btn btn-lg btn-block"),a.disabled=r[1],H(a,"btn-loading",r[1])},m(f,w){_(f,e,w),k(e,s),k(s,t),k(t,l),p&&p.m(t,null),k(e,n),U(o,e,null),k(e,c),k(e,a),k(a,i),u=!0,g||(q=E(e,"submit",D(r[4])),g=!0)},p(f,w){f[3]?p?p.p(f,w):(p=L(f),p.c(),p.m(t,null)):p&&(p.d(1),p=null);const P={};w&769&&(P.$$scope={dirty:w,ctx:f}),o.$set(P),(!u||w&2)&&(a.disabled=f[1]),w&2&&H(a,"btn-loading",f[1])},i(f){u||($(o.$$.fragment,f),u=!0)},o(f){v(o.$$.fragment,f),u=!1},d(f){f&&b(e),p&&p.d(),z(o),g=!1,q()}}}function K(r){let e,s,t,l,n;return{c(){e=m("div"),e.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
|
||||
<div class="content txt-bold"><p>Email address changed</p>
|
||||
<p>You can now sign in with your new email address.</p></div>`,s=C(),t=m("button"),t.textContent="Close",d(e,"class","alert alert-success"),d(t,"type","button"),d(t,"class","btn btn-secondary btn-block")},m(o,c){_(o,e,c),_(o,s,c),_(o,t,c),l||(n=E(t,"click",r[6]),l=!0)},p:h,i:h,o:h,d(o){o&&b(e),o&&b(s),o&&b(t),l=!1,n()}}}function L(r){let e,s,t;return{c(){e=y("to "),s=m("strong"),t=y(r[3]),d(s,"class","txt-nowrap")},m(l,n){_(l,e,n),_(l,s,n),k(s,t)},p(l,n){n&8&&G(t,l[3])},d(l){l&&b(e),l&&b(s)}}}function O(r){let e,s,t,l,n,o,c,a;return{c(){e=m("label"),s=y("Password"),l=C(),n=m("input"),d(e,"for",t=r[8]),d(n,"type","password"),d(n,"id",o=r[8]),n.required=!0,n.autofocus=!0},m(i,u){_(i,e,u),k(e,s),_(i,l,u),_(i,n,u),T(n,r[0]),n.focus(),c||(a=E(n,"input",r[7]),c=!0)},p(i,u){u&256&&t!==(t=i[8])&&d(e,"for",t),u&256&&o!==(o=i[8])&&d(n,"id",o),u&1&&n.value!==i[0]&&T(n,i[0])},d(i){i&&b(e),i&&b(l),i&&b(n),c=!1,a()}}}function Q(r){let e,s,t,l;const n=[K,I],o=[];function c(a,i){return a[2]?0:1}return e=c(r),s=o[e]=n[e](r),{c(){s.c(),t=Y()},m(a,i){o[e].m(a,i),_(a,t,i),l=!0},p(a,i){let u=e;e=c(a),e===u?o[e].p(a,i):(j(),v(o[u],1,1,()=>{o[u]=null}),A(),s=o[e],s?s.p(a,i):(s=o[e]=n[e](a),s.c()),$(s,1),s.m(t.parentNode,t))},i(a){l||($(s),l=!0)},o(a){v(s),l=!1},d(a){o[e].d(a),a&&b(t)}}}function V(r){let e,s;return e=new R({props:{nobranding:!0,$$slots:{default:[Q]},$$scope:{ctx:r}}}),{c(){S(e.$$.fragment)},m(t,l){U(e,t,l),s=!0},p(t,[l]){const n={};l&527&&(n.$$scope={dirty:l,ctx:t}),e.$set(n)},i(t){s||($(e.$$.fragment,t),s=!0)},o(t){v(e.$$.fragment,t),s=!1},d(t){z(e,t)}}}function X(r,e,s){let t,{params:l}=e,n="",o=!1,c=!1;async function a(){if(!o){s(1,o=!0);try{await F.Users.confirmEmailChange(l==null?void 0:l.token,n),s(2,c=!0)}catch(g){F.errorResponseHandler(g)}s(1,o=!1)}}const i=()=>window.close();function u(){n=this.value,s(0,n)}return r.$$set=g=>{"params"in g&&s(5,l=g.params)},r.$$.update=()=>{r.$$.dirty&32&&s(3,t=W.getJWTPayload(l==null?void 0:l.token).newEmail||"")},[n,o,c,t,a,l,i,u]}class x extends J{constructor(e){super(),M(this,e,X,V,N,{params:5})}}export{x as default};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as W,i as Y,s as j,F as A,c as H,m as N,t as P,a as q,d as S,C as B,E as D,g as _,k as G,n as I,o as m,p as z,q as E,e as b,w as y,b as C,f as c,r as J,h as w,u as h,v as K,y as F,x as O,z as R}from"./index.0d48f812.js";function Q(i){let e,l,t,n,s,o,p,a,r,u,v,g,k,L,d=i[4]&&M(i);return o=new E({props:{class:"form-field required",name:"password",$$slots:{default:[X,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:i}}}),a=new E({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[Z,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:i}}}),{c(){e=b("form"),l=b("div"),t=b("h4"),n=y(`Reset your user password
|
||||
import{S as W,i as Y,s as j,F as A,c as H,m as N,t as P,a as q,d as S,C as B,E as D,g as _,k as G,n as I,o as m,p as z,q as E,e as b,w as y,b as C,f as c,r as J,h as w,u as h,v as K,y as F,x as O,z as R}from"./index.38300823.js";function Q(i){let e,l,t,n,s,o,p,a,r,u,v,g,k,L,d=i[4]&&M(i);return o=new E({props:{class:"form-field required",name:"password",$$slots:{default:[X,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:i}}}),a=new E({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[Z,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:i}}}),{c(){e=b("form"),l=b("div"),t=b("h4"),n=y(`Reset your user password
|
||||
`),d&&d.c(),s=C(),H(o.$$.fragment),p=C(),H(a.$$.fragment),r=C(),u=b("button"),v=b("span"),v.textContent="Set new password",c(t,"class","m-b-xs"),c(l,"class","content txt-center m-b-sm"),c(v,"class","txt"),c(u,"type","submit"),c(u,"class","btn btn-lg btn-block"),u.disabled=i[2],J(u,"btn-loading",i[2])},m(f,$){_(f,e,$),w(e,l),w(l,t),w(t,n),d&&d.m(t,null),w(e,s),N(o,e,null),w(e,p),N(a,e,null),w(e,r),w(e,u),w(u,v),g=!0,k||(L=h(e,"submit",K(i[5])),k=!0)},p(f,$){f[4]?d?d.p(f,$):(d=M(f),d.c(),d.m(t,null)):d&&(d.d(1),d=null);const T={};$&3073&&(T.$$scope={dirty:$,ctx:f}),o.$set(T);const U={};$&3074&&(U.$$scope={dirty:$,ctx:f}),a.$set(U),(!g||$&4)&&(u.disabled=f[2]),$&4&&J(u,"btn-loading",f[2])},i(f){g||(P(o.$$.fragment,f),P(a.$$.fragment,f),g=!0)},o(f){q(o.$$.fragment,f),q(a.$$.fragment,f),g=!1},d(f){f&&m(e),d&&d.d(),S(o),S(a),k=!1,L()}}}function V(i){let e,l,t,n,s;return{c(){e=b("div"),e.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
|
||||
<div class="content txt-bold"><p>Password changed</p>
|
||||
<p>You can now sign in with your new password.</p></div>`,l=C(),t=b("button"),t.textContent="Close",c(e,"class","alert alert-success"),c(t,"type","button"),c(t,"class","btn btn-secondary btn-block")},m(o,p){_(o,e,p),_(o,l,p),_(o,t,p),n||(s=h(t,"click",i[7]),n=!0)},p:F,i:F,o:F,d(o){o&&m(e),o&&m(l),o&&m(t),n=!1,s()}}}function M(i){let e,l,t;return{c(){e=y("for "),l=b("strong"),t=y(i[4])},m(n,s){_(n,e,s),_(n,l,s),w(l,t)},p(n,s){s&16&&O(t,n[4])},d(n){n&&m(e),n&&m(l)}}}function X(i){let e,l,t,n,s,o,p,a;return{c(){e=b("label"),l=y("New password"),n=C(),s=b("input"),c(e,"for",t=i[10]),c(s,"type","password"),c(s,"id",o=i[10]),s.required=!0,s.autofocus=!0},m(r,u){_(r,e,u),w(e,l),_(r,n,u),_(r,s,u),R(s,i[0]),s.focus(),p||(a=h(s,"input",i[8]),p=!0)},p(r,u){u&1024&&t!==(t=r[10])&&c(e,"for",t),u&1024&&o!==(o=r[10])&&c(s,"id",o),u&1&&s.value!==r[0]&&R(s,r[0])},d(r){r&&m(e),r&&m(n),r&&m(s),p=!1,a()}}}function Z(i){let e,l,t,n,s,o,p,a;return{c(){e=b("label"),l=y("New password confirm"),n=C(),s=b("input"),c(e,"for",t=i[10]),c(s,"type","password"),c(s,"id",o=i[10]),s.required=!0},m(r,u){_(r,e,u),w(e,l),_(r,n,u),_(r,s,u),R(s,i[1]),p||(a=h(s,"input",i[9]),p=!0)},p(r,u){u&1024&&t!==(t=r[10])&&c(e,"for",t),u&1024&&o!==(o=r[10])&&c(s,"id",o),u&2&&s.value!==r[1]&&R(s,r[1])},d(r){r&&m(e),r&&m(n),r&&m(s),p=!1,a()}}}function x(i){let e,l,t,n;const s=[V,Q],o=[];function p(a,r){return a[3]?0:1}return e=p(i),l=o[e]=s[e](i),{c(){l.c(),t=D()},m(a,r){o[e].m(a,r),_(a,t,r),n=!0},p(a,r){let u=e;e=p(a),e===u?o[e].p(a,r):(G(),q(o[u],1,1,()=>{o[u]=null}),I(),l=o[e],l?l.p(a,r):(l=o[e]=s[e](a),l.c()),P(l,1),l.m(t.parentNode,t))},i(a){n||(P(l),n=!0)},o(a){q(l),n=!1},d(a){o[e].d(a),a&&m(t)}}}function ee(i){let e,l;return e=new A({props:{nobranding:!0,$$slots:{default:[x]},$$scope:{ctx:i}}}),{c(){H(e.$$.fragment)},m(t,n){N(e,t,n),l=!0},p(t,[n]){const s={};n&2079&&(s.$$scope={dirty:n,ctx:t}),e.$set(s)},i(t){l||(P(e.$$.fragment,t),l=!0)},o(t){q(e.$$.fragment,t),l=!1},d(t){S(e,t)}}}function te(i,e,l){let t,{params:n}=e,s="",o="",p=!1,a=!1;async function r(){if(!p){l(2,p=!0);try{await z.Users.confirmPasswordReset(n==null?void 0:n.token,s,o),l(3,a=!0)}catch(k){z.errorResponseHandler(k)}l(2,p=!1)}}const u=()=>window.close();function v(){s=this.value,l(0,s)}function g(){o=this.value,l(1,o)}return i.$$set=k=>{"params"in k&&l(6,n=k.params)},i.$$.update=()=>{i.$$.dirty&64&&l(4,t=B.getJWTPayload(n==null?void 0:n.token).email||"")},[s,o,p,a,t,r,n,u,v,g]}class le extends W{constructor(e){super(),Y(this,e,te,ee,j,{params:6})}}export{le as default};
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
import{S as k,i as v,s as y,F as w,c as x,m as C,t as g,a as $,d as L,p as H,E as M,g as r,o as a,e as u,b as m,f,u as _,y as p}from"./index.0d48f812.js";function P(o){let t,s,e,n,i;return{c(){t=u("div"),t.innerHTML=`<div class="icon"><i class="ri-error-warning-line"></i></div>
|
||||
import{S as k,i as v,s as y,F as w,c as x,m as C,t as g,a as $,d as L,p as H,E as M,g as r,o as a,e as u,b as m,f,u as _,y as p}from"./index.38300823.js";function P(o){let t,s,e,n,i;return{c(){t=u("div"),t.innerHTML=`<div class="icon"><i class="ri-error-warning-line"></i></div>
|
||||
<div class="content txt-bold"><p>Invalid or expired verification token.</p></div>`,s=m(),e=u("button"),e.textContent="Close",f(t,"class","alert alert-danger"),f(e,"type","button"),f(e,"class","btn btn-secondary btn-block")},m(l,c){r(l,t,c),r(l,s,c),r(l,e,c),n||(i=_(e,"click",o[4]),n=!0)},p,d(l){l&&a(t),l&&a(s),l&&a(e),n=!1,i()}}}function S(o){let t,s,e,n,i;return{c(){t=u("div"),t.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
|
||||
<div class="content txt-bold"><p>Successfully verified email address.</p></div>`,s=m(),e=u("button"),e.textContent="Close",f(t,"class","alert alert-success"),f(e,"type","button"),f(e,"class","btn btn-secondary btn-block")},m(l,c){r(l,t,c),r(l,s,c),r(l,e,c),n||(i=_(e,"click",o[3]),n=!0)},p,d(l){l&&a(t),l&&a(s),l&&a(e),n=!1,i()}}}function T(o){let t;return{c(){t=u("div"),t.innerHTML='<div class="loader loader-lg"><em>Please wait...</em></div>',f(t,"class","txt-center")},m(s,e){r(s,t,e)},p,d(s){s&&a(t)}}}function F(o){let t;function s(i,l){return i[1]?T:i[0]?S:P}let e=s(o),n=e(o);return{c(){n.c(),t=M()},m(i,l){n.m(i,l),r(i,t,l)},p(i,l){e===(e=s(i))&&n?n.p(i,l):(n.d(1),n=e(i),n&&(n.c(),n.m(t.parentNode,t)))},d(i){n.d(i),i&&a(t)}}}function U(o){let t,s;return t=new w({props:{nobranding:!0,$$slots:{default:[F]},$$scope:{ctx:o}}}),{c(){x(t.$$.fragment)},m(e,n){C(t,e,n),s=!0},p(e,[n]){const i={};n&67&&(i.$$scope={dirty:n,ctx:e}),t.$set(i)},i(e){s||(g(t.$$.fragment,e),s=!0)},o(e){$(t.$$.fragment,e),s=!1},d(e){L(t,e)}}}function V(o,t,s){let{params:e}=t,n=!1,i=!1;l();async function l(){s(1,i=!0);try{await H.Users.confirmVerification(e==null?void 0:e.token),s(0,n=!0)}catch(d){console.warn(d),s(0,n=!1)}s(1,i=!1)}const c=()=>window.close(),b=()=>window.close();return o.$$set=d=>{"params"in d&&s(2,e=d.params)},[n,i,e,c,b]}class E extends k{constructor(t){super(),v(this,t,V,U,y,{params:2})}}export{E as default};
|
||||
+148
-143
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -20,8 +20,8 @@
|
||||
window.Prism = window.Prism || {};
|
||||
window.Prism.manual = true;
|
||||
</script>
|
||||
<script type="module" crossorigin src="/_/assets/index.0d48f812.js"></script>
|
||||
<link rel="stylesheet" href="/_/assets/index.15ba3d82.css">
|
||||
<script type="module" crossorigin src="/_/assets/index.38300823.js"></script>
|
||||
<link rel="stylesheet" href="/_/assets/index.615ff839.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import { errors } from "@/stores/errors";
|
||||
import Toggler from "@/components/base/Toggler.svelte";
|
||||
import Accordion from "@/components/base/Accordion.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import FieldTypeSelect from "@/components/collections/schema/FieldTypeSelect.svelte";
|
||||
@@ -272,13 +273,16 @@
|
||||
{#if !field.toDelete}
|
||||
<div class="col-sm-4 txt-right">
|
||||
<div class="flex-fill" />
|
||||
<div class="inline-flex flex-gap-base flex-nowrap">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-secondary fade"
|
||||
on:click|stopPropagation={handleDelete}
|
||||
>
|
||||
<span class="txt">Remove</span>
|
||||
<div class="inline-flex flex-gap-sm flex-nowrap">
|
||||
<button type="button" class="btn btn-circle btn-sm btn-secondary">
|
||||
<i class="ri-more-line" />
|
||||
<Toggler
|
||||
class="dropdown dropdown-sm dropdown-upside dropdown-right dropdown-nowrap no-min-width"
|
||||
>
|
||||
<button type="button" class="dropdown-item txt-right" on:click={handleDelete}>
|
||||
<span class="txt">Remove</span>
|
||||
</button>
|
||||
</Toggler>
|
||||
</button>
|
||||
|
||||
{#if interactive}
|
||||
|
||||
@@ -53,14 +53,11 @@
|
||||
|
||||
const client = new PocketBase("${ApiClient.baseUrl}");
|
||||
|
||||
...
|
||||
|
||||
const data = { ... };
|
||||
|
||||
client.Records.create("${collection?.name}", data)
|
||||
.then(function (record) {
|
||||
// success...
|
||||
}).catch(function (error) {
|
||||
// error...
|
||||
});
|
||||
const record = await client.Records.create("${collection?.name}", data);
|
||||
`,
|
||||
},
|
||||
];
|
||||
@@ -161,6 +158,37 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Query parameters</div>
|
||||
<table class="table-compact table-border m-b-lg">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>expand</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
Auto expand relations when returning the created record. Ex.:
|
||||
<CodeBlock
|
||||
content={`
|
||||
?expand=rel1,rel2.subrel21.subrel22
|
||||
`}
|
||||
/>
|
||||
Supports up to 6-levels depth nested relations expansion. <br />
|
||||
The expanded relations will be appended to the record under the
|
||||
<code>@expand</code> property (eg. <code>{`"@expand": {"rel1": {...}, ...}`}</code>). Only the
|
||||
relations that the user has permissions to <strong>view</strong> will be expanded.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
|
||||
@@ -64,12 +64,9 @@
|
||||
|
||||
const client = new PocketBase("${ApiClient.baseUrl}");
|
||||
|
||||
client.Records.delete("${collection?.name}", "RECORD_ID")
|
||||
.then(function () {
|
||||
// success...
|
||||
}).catch(function (error) {
|
||||
// error...
|
||||
});
|
||||
...
|
||||
|
||||
await client.Records.delete("${collection?.name}", "RECORD_ID");
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -76,20 +76,17 @@
|
||||
|
||||
const client = new PocketBase("${ApiClient.baseUrl}");
|
||||
|
||||
client.Records.getList("${collection?.name}", { page: 2 })
|
||||
.then(function (list) {
|
||||
// success...
|
||||
}).catch(function (error) {
|
||||
// error...
|
||||
});
|
||||
...
|
||||
|
||||
// fetch a paginated records list
|
||||
const resultList = await client.Records.getList("${collection?.name}", 1, 50, {
|
||||
filter: "created >= '2022-01-01 00:00:00'",
|
||||
});
|
||||
|
||||
// alternatively you can also fetch all records at once via getFullList:
|
||||
client.Records.getFullList("${collection?.name}", 200 /* batch size */)
|
||||
.then(function (records) {
|
||||
// success...
|
||||
}).catch(function (error) {
|
||||
// error...
|
||||
});
|
||||
const records = await client.Records.getFullList("${collection?.name}", 200 /* batch size */, {
|
||||
sort: "-created",
|
||||
});
|
||||
`,
|
||||
},
|
||||
];
|
||||
@@ -195,7 +192,7 @@
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
Auto expand nested record relations. Ex.:
|
||||
Auto expand record relations. Ex.:
|
||||
<CodeBlock
|
||||
content={`
|
||||
?expand=rel1,rel2.subrel21.subrel22
|
||||
|
||||
@@ -63,14 +63,11 @@
|
||||
|
||||
const client = new PocketBase("${ApiClient.baseUrl}");
|
||||
|
||||
...
|
||||
|
||||
const data = { ... };
|
||||
|
||||
client.Records.update("${collection?.name}", "RECORD_ID", data)
|
||||
.then(function (record) {
|
||||
// success...
|
||||
}).catch(function (error) {
|
||||
// error...
|
||||
});
|
||||
const record = await client.Records.update("${collection?.name}", "RECORD_ID", data);
|
||||
`,
|
||||
},
|
||||
];
|
||||
@@ -191,6 +188,37 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Query parameters</div>
|
||||
<table class="table-compact table-border m-b-lg">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>expand</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
Auto expand relations when returning the updated record. Ex.:
|
||||
<CodeBlock
|
||||
content={`
|
||||
?expand=rel1,rel2.subrel21.subrel22
|
||||
`}
|
||||
/>
|
||||
Supports up to 6-levels depth nested relations expansion. <br />
|
||||
The expanded relations will be appended to the record under the
|
||||
<code>@expand</code> property (eg. <code>{`"@expand": {"rel1": {...}, ...}`}</code>). Only the
|
||||
relations that the user has permissions to <strong>view</strong> will be expanded.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
|
||||
@@ -52,12 +52,11 @@
|
||||
|
||||
const client = new PocketBase("${ApiClient.baseUrl}");
|
||||
|
||||
client.Records.getOne("${collection?.name}", "RECORD_ID")
|
||||
.then(function (record) {
|
||||
// success...
|
||||
}).catch(function (error) {
|
||||
// error...
|
||||
});
|
||||
...
|
||||
|
||||
const record = await client.Records.getOne("${collection?.name}", "RECORD_ID", {
|
||||
expand: "some_relation"
|
||||
});
|
||||
`,
|
||||
},
|
||||
];
|
||||
@@ -137,7 +136,7 @@
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
Auto expand nested record relations. Ex.:
|
||||
Auto expand record relations. Ex.:
|
||||
<CodeBlock
|
||||
content={`
|
||||
?expand=rel1,rel2.subrel21.subrel22
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
title={"Download " + filename}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
download
|
||||
>
|
||||
/.../{filename}
|
||||
</a>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import RedactedPasswordInput from "@/components/base/RedactedPasswordInput.svelte";
|
||||
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
|
||||
$pageTitle = "Files storage";
|
||||
|
||||
@@ -155,6 +156,21 @@
|
||||
<RedactedPasswordInput id={uniqueId} required bind:value={s3.secret} />
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-lg-12">
|
||||
<Field class="form-field" name="s3.forcePathStyle" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={s3.forcePathStyle} />
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Force path-style addressing</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: 'Forces the request to use path-style addressing, eg. "https://s3.amazonaws.com/BUCKET/KEY" instead of the default "https://BUCKET.s3.amazonaws.com/KEY".',
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</Field>
|
||||
</div>
|
||||
<!-- margin helper -->
|
||||
<div class="col-lg-12" />
|
||||
</div>
|
||||
|
||||
@@ -392,6 +392,9 @@ a,
|
||||
.p-#{$sideAlias}-#{$spacing} { padding-#{$side}: $spacing * 1px !important; }
|
||||
}
|
||||
}
|
||||
.no-min-width {
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
|
||||
@@ -226,6 +226,7 @@ button {
|
||||
&.btn-circle {
|
||||
border-radius: 50%;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
i {
|
||||
$iconSize: 23px;
|
||||
font-size: 1.2857rem;
|
||||
|
||||
@@ -196,6 +196,7 @@
|
||||
&.image-preview {
|
||||
width: auto;
|
||||
min-width: 300px;
|
||||
min-height: 250px;
|
||||
max-width: 70%;
|
||||
max-height: 90%;
|
||||
.panel-header {
|
||||
@@ -220,6 +221,9 @@
|
||||
.panel-content {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
|
||||
@@ -789,7 +789,7 @@ export default class CommonHelper {
|
||||
* @return {Boolean}
|
||||
*/
|
||||
static hasImageExtension(filename) {
|
||||
return /\.jpg|\.jpeg|\.png|\.svg|\.webp|\.avif$/.test(filename)
|
||||
return /\.jpg|\.jpeg|\.png|\.svg|\.gif|\.webp|\.avif$/.test(filename)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user