Skip to content

Commit

Permalink
added view collection type
Browse files Browse the repository at this point in the history
  • Loading branch information
ganigeorgiev committed Feb 18, 2023
1 parent 0052e2a commit a07f670
Show file tree
Hide file tree
Showing 98 changed files with 3,260 additions and 830 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@

- Enabled `process.env` in js migrations to allow accessing `os.Environ()`.

- Enabled file thumbs when visualizing `relation` display file fields.

- Added new "View" collection type (@todo document)


## v0.12.3

Expand Down
155 changes: 139 additions & 16 deletions apis/collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func TestCollectionsList(t *testing.T) {
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalItems":8`,
`"totalItems":10`,
`"items":[{`,
`"id":"_pb_users_auth_"`,
`"id":"v851q4r790rhknl"`,
Expand Down Expand Up @@ -73,10 +73,10 @@ func TestCollectionsList(t *testing.T) {
ExpectedContent: []string{
`"page":2`,
`"perPage":2`,
`"totalItems":8`,
`"totalItems":10`,
`"items":[{`,
`"id":"v851q4r790rhknl"`,
`"id":"4d1blo5cuycfaca"`,
`"id":"kpv709sk2lqbqk8"`,
`"id":"9n89pl5vkct6330"`,
},
ExpectedEvents: map[string]int{
"OnCollectionsListRequest": 1,
Expand Down Expand Up @@ -231,7 +231,7 @@ func TestCollectionDelete(t *testing.T) {
{
Name: "authorized as admin + using the collection name",
Method: http.MethodDelete,
Url: "/api/collections/demo1",
Url: "/api/collections/demo5",
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
},
Expand All @@ -244,13 +244,13 @@ func TestCollectionDelete(t *testing.T) {
"OnCollectionAfterDeleteRequest": 1,
},
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
ensureDeletedFiles(app, "wsmn24bux7wo113")
ensureDeletedFiles(app, "9n89pl5vkct6330")
},
},
{
Name: "authorized as admin + using the collection id",
Method: http.MethodDelete,
Url: "/api/collections/wsmn24bux7wo113",
Url: "/api/collections/9n89pl5vkct6330",
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
},
Expand All @@ -263,7 +263,7 @@ func TestCollectionDelete(t *testing.T) {
"OnCollectionAfterDeleteRequest": 1,
},
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
ensureDeletedFiles(app, "wsmn24bux7wo113")
ensureDeletedFiles(app, "9n89pl5vkct6330")
},
},
{
Expand Down Expand Up @@ -292,6 +292,22 @@ func TestCollectionDelete(t *testing.T) {
"OnCollectionBeforeDeleteRequest": 1,
},
},
{
Name: "authorized as admin + deleting a view",
Method: http.MethodDelete,
Url: "/api/collections/view2",
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
},
Delay: 100 * time.Millisecond,
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"OnModelBeforeDelete": 1,
"OnModelAfterDelete": 1,
"OnCollectionBeforeDeleteRequest": 1,
"OnCollectionAfterDeleteRequest": 1,
},
},
}

for _, scenario := range scenarios {
Expand Down Expand Up @@ -520,6 +536,56 @@ func TestCollectionCreate(t *testing.T) {
`"options":{"minPasswordLength":{"code":"validation_required"`,
},
},

// view
// -----------------------------------------------------------
{
Name: "trying to create view collection with invalid options",
Method: http.MethodPost,
Url: "/api/collections",
Body: strings.NewReader(`{
"name":"new",
"type":"view",
"schema":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
"options":{"query": "invalid"}
}`),
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"options":{"query":{"code":"validation_invalid_view_query`,
},
},
{
Name: "creating view collection",
Method: http.MethodPost,
Url: "/api/collections",
Body: strings.NewReader(`{
"name":"new",
"type":"view",
"schema":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
"options": {
"query": "select 1 as id from _admins"
}
}`),
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"name":"new"`,
`"type":"view"`,
`"schema":[]`,
},
ExpectedEvents: map[string]int{
"OnModelBeforeCreate": 1,
"OnModelAfterCreate": 1,
"OnCollectionBeforeCreateRequest": 1,
"OnCollectionAfterCreateRequest": 1,
},
},
}

for _, scenario := range scenarios {
Expand Down Expand Up @@ -660,7 +726,7 @@ func TestCollectionUpdate(t *testing.T) {
{
Name: "updating base collection with reserved auth fields",
Method: http.MethodPatch,
Url: "/api/collections/demo1",
Url: "/api/collections/demo4",
Body: strings.NewReader(`{
"schema":[
{"type":"text","name":"email"},
Expand All @@ -681,7 +747,7 @@ func TestCollectionUpdate(t *testing.T) {
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"name":"demo1"`,
`"name":"demo4"`,
`"type":"base"`,
`"schema":[{`,
`"email"`,
Expand Down Expand Up @@ -751,6 +817,7 @@ func TestCollectionUpdate(t *testing.T) {
},

// rel field change displayFields propagation
// -----------------------------------------------------------
{
Name: "renaming a display field should also update the referenced displayFields value",
Method: http.MethodPatch,
Expand Down Expand Up @@ -830,6 +897,60 @@ func TestCollectionUpdate(t *testing.T) {
}
},
},

// view
// -----------------------------------------------------------
{
Name: "trying to update view collection with invalid options",
Method: http.MethodPatch,
Url: "/api/collections/view1",
Body: strings.NewReader(`{
"schema":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
"options":{"query": "invalid"}
}`),
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"options":{"query":{"code":"validation_invalid_view_query`,
},
},
{
Name: "updating view collection",
Method: http.MethodPatch,
Url: "/api/collections/view2",
Body: strings.NewReader(`{
"name":"view2_update",
"schema":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
"options": {
"query": "select 2 as id, created, updated, email from _admins"
}
}`),
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"name":"view2_update"`,
`"type":"view"`,
`"schema":[{`,
`"name":"email"`,
},
NotExpectedContent: []string{
// base model fields are not part of the schema
`"name":"id"`,
`"name":"created"`,
`"name":"updated"`,
},
ExpectedEvents: map[string]int{
"OnModelBeforeUpdate": 1,
"OnModelAfterUpdate": 1,
"OnCollectionBeforeUpdateRequest": 1,
"OnCollectionAfterUpdateRequest": 1,
},
},
}

for _, scenario := range scenarios {
Expand All @@ -838,6 +959,8 @@ func TestCollectionUpdate(t *testing.T) {
}

func TestCollectionImport(t *testing.T) {
totalCollections := 10

scenarios := []tests.ApiScenario{
{
Name: "unauthorized",
Expand Down Expand Up @@ -874,7 +997,7 @@ func TestCollectionImport(t *testing.T) {
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
}
expected := 8
expected := totalCollections
if len(collections) != expected {
t.Fatalf("Expected %d collections, got %d", expected, len(collections))
}
Expand Down Expand Up @@ -902,7 +1025,7 @@ func TestCollectionImport(t *testing.T) {
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
}
expected := 8
expected := totalCollections
if len(collections) != expected {
t.Fatalf("Expected %d collections, got %d", expected, len(collections))
}
Expand Down Expand Up @@ -944,7 +1067,7 @@ func TestCollectionImport(t *testing.T) {
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
}
expected := 8
expected := totalCollections
if len(collections) != expected {
t.Fatalf("Expected %d collections, got %d", expected, len(collections))
}
Expand Down Expand Up @@ -997,7 +1120,7 @@ func TestCollectionImport(t *testing.T) {
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
}
expected := 11
expected := totalCollections + 3
if len(collections) != expected {
t.Fatalf("Expected %d collections, got %d", expected, len(collections))
}
Expand Down Expand Up @@ -1084,8 +1207,8 @@ func TestCollectionImport(t *testing.T) {
ExpectedEvents: map[string]int{
"OnCollectionsAfterImportRequest": 1,
"OnCollectionsBeforeImportRequest": 1,
"OnModelBeforeDelete": 6,
"OnModelAfterDelete": 6,
"OnModelBeforeDelete": 8,
"OnModelAfterDelete": 8,
"OnModelBeforeUpdate": 2,
"OnModelAfterUpdate": 2,
"OnModelBeforeCreate": 1,
Expand Down
18 changes: 16 additions & 2 deletions apis/file.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package apis

import (
"fmt"

"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/models"
Expand Down Expand Up @@ -45,15 +47,27 @@ func (api *fileApi) download(c echo.Context) error {
if fileField == nil {
return NewNotFoundError("", nil)
}

options, _ := fileField.Options.(*schema.FileOptions)

baseFilesPath := record.BaseFilesPath()

// fetch the original view file field related record
if collection.IsView() {
fileRecord, err := api.app.Dao().FindRecordByViewFile(collection.Id, fileField.Name, filename)
if err != nil {
return NewNotFoundError("", fmt.Errorf("Failed to fetch view file field record: %w", err))
}
baseFilesPath = fileRecord.BaseFilesPath()
}

fs, err := api.app.NewFilesystem()
if err != nil {
return NewBadRequestError("Filesystem initialization failure.", err)
}
defer fs.Close()

originalPath := record.BaseFilesPath() + "/" + filename
originalPath := baseFilesPath + "/" + filename
servedPath := originalPath
servedName := filename

Expand All @@ -70,7 +84,7 @@ func (api *fileApi) download(c echo.Context) error {
if list.ExistInSlice(oAttrs.ContentType, imageContentTypes) {
// add thumb size as file suffix
servedName = thumbSize + "_" + filename
servedPath = record.BaseFilesPath() + "/thumbs_" + filename + "/" + servedName
servedPath = baseFilesPath + "/thumbs_" + filename + "/" + servedName

// check if the thumb exists:
// - if doesn't exist - create a new thumb with the specified thumb size
Expand Down
10 changes: 6 additions & 4 deletions apis/middlewares.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,12 @@ func RequireGuestOnly() echo.MiddlewareFunc {
// specifying their names.
//
// Example:
// apis.RequireRecordAuth()
//
// apis.RequireRecordAuth()
//
// Or:
// apis.RequireRecordAuth("users", "supervisors")
//
// apis.RequireRecordAuth("users", "supervisors")
//
// To restrict the auth record only to the loaded context collection,
// use [apis.RequireSameContextRecordAuth()] instead.
Expand All @@ -83,7 +86,6 @@ func RequireRecordAuth(optCollectionNames ...string) echo.MiddlewareFunc {
}
}

//
// RequireSameContextRecordAuth middleware requires a request to have
// a valid record Authorization header.
//
Expand Down Expand Up @@ -261,7 +263,7 @@ func LoadCollectionContext(app core.App, optCollectionTypes ...string) echo.Midd
}

if len(optCollectionTypes) > 0 && !list.ExistInSlice(collection.Type, optCollectionTypes) {
return NewBadRequestError("Invalid collection type.", nil)
return NewBadRequestError("Unsupported collection type.", nil)
}

c.Set(ContextCollectionKey, collection)
Expand Down
11 changes: 5 additions & 6 deletions apis/record_crud.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,13 @@ func bindRecordCrudApi(app core.App, rg *echo.Group) {
subGroup := rg.Group(
"/collections/:collection",
ActivityLogger(app),
LoadCollectionContext(app),
)

subGroup.GET("/records", api.list)
subGroup.POST("/records", api.create)
subGroup.GET("/records/:id", api.view)
subGroup.PATCH("/records/:id", api.update)
subGroup.DELETE("/records/:id", api.delete)
subGroup.GET("/records", api.list, LoadCollectionContext(app))
subGroup.GET("/records/:id", api.view, LoadCollectionContext(app))
subGroup.POST("/records", api.create, LoadCollectionContext(app, models.CollectionTypeBase, models.CollectionTypeAuth))
subGroup.PATCH("/records/:id", api.update, LoadCollectionContext(app, models.CollectionTypeBase, models.CollectionTypeAuth))
subGroup.DELETE("/records/:id", api.delete, LoadCollectionContext(app, models.CollectionTypeBase, models.CollectionTypeAuth))
}

type recordApi struct {
Expand Down
Loading

0 comments on commit a07f670

Please sign in to comment.