1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-01-08 09:14:37 +02:00
pocketbase/apis/record_crud.go

422 lines
13 KiB
Go
Raw Normal View History

2022-07-06 23:19:05 +02:00
package apis
import (
"fmt"
"log"
"net/http"
"strings"
"github.com/labstack/echo/v5"
"github.com/pocketbase/dbx"
2022-07-06 23:19:05 +02:00
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/forms"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/resolvers"
"github.com/pocketbase/pocketbase/tools/search"
)
const expandQueryParam = "expand"
2022-10-30 10:28:14 +02:00
// bindRecordCrudApi registers the record crud api endpoints and
// the corresponding handlers.
func bindRecordCrudApi(app core.App, rg *echo.Group) {
2022-07-06 23:19:05 +02:00
api := recordApi{app: app}
subGroup := rg.Group(
2022-10-30 10:28:14 +02:00
"/collections/:collection",
2022-07-06 23:19:05 +02:00
ActivityLogger(app),
)
2023-02-18 19:33:42 +02:00
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))
2022-07-06 23:19:05 +02:00
}
type recordApi struct {
app core.App
}
func (api *recordApi) list(c echo.Context) error {
collection, _ := c.Get(ContextCollectionKey).(*models.Collection)
if collection == nil {
2022-10-30 10:28:14 +02:00
return NewNotFoundError("", "Missing collection context.")
2022-07-06 23:19:05 +02:00
}
// forbid users and guests to query special filter/sort fields
if err := api.checkForForbiddenQueryFields(c); err != nil {
return err
2022-07-06 23:19:05 +02:00
}
requestInfo := RequestInfo(c)
if requestInfo.Admin == nil && collection.ListRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
2022-07-06 23:19:05 +02:00
2022-10-30 10:28:14 +02:00
fieldsResolver := resolvers.NewRecordFieldResolver(
api.app.Dao(),
collection,
requestInfo,
2022-10-30 10:28:14 +02:00
// hidden fields are searchable only by admins
requestInfo.Admin != nil,
2022-10-30 10:28:14 +02:00
)
2022-07-06 23:19:05 +02:00
searchProvider := search.NewProvider(fieldsResolver).
2022-10-30 10:28:14 +02:00
Query(api.app.Dao().RecordQuery(collection))
2022-07-06 23:19:05 +02:00
// views don't have "rowid" so we fallback to "id"
if collection.IsView() {
searchProvider.CountCol("id")
}
if requestInfo.Admin == nil && collection.ListRule != nil {
2022-07-06 23:19:05 +02:00
searchProvider.AddFilter(search.FilterData(*collection.ListRule))
}
2023-02-21 16:38:12 +02:00
records := []*models.Record{}
result, err := searchProvider.ParseAndExec(c.QueryParams().Encode(), &records)
2022-07-06 23:19:05 +02:00
if err != nil {
2022-10-30 10:28:14 +02:00
return NewBadRequestError("Invalid filter parameters.", err)
2022-07-06 23:19:05 +02:00
}
event := new(core.RecordsListEvent)
event.HttpContext = c
event.Collection = collection
event.Records = records
event.Result = result
2022-07-06 23:19:05 +02:00
return api.app.OnRecordsListRequest().Trigger(event, func(e *core.RecordsListEvent) error {
2023-07-20 09:40:03 +02:00
if e.HttpContext.Response().Committed {
return nil
}
if err := EnrichRecords(e.HttpContext, api.app.Dao(), e.Records); err != nil && api.app.IsDebug() {
log.Println(err)
}
2022-07-06 23:19:05 +02:00
return e.HttpContext.JSON(http.StatusOK, e.Result)
})
}
func (api *recordApi) view(c echo.Context) error {
collection, _ := c.Get(ContextCollectionKey).(*models.Collection)
if collection == nil {
2022-10-30 10:28:14 +02:00
return NewNotFoundError("", "Missing collection context.")
2022-07-06 23:19:05 +02:00
}
recordId := c.PathParam("id")
if recordId == "" {
2022-10-30 10:28:14 +02:00
return NewNotFoundError("", nil)
2022-07-06 23:19:05 +02:00
}
requestInfo := RequestInfo(c)
if requestInfo.Admin == nil && collection.ViewRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
2022-07-06 23:19:05 +02:00
ruleFunc := func(q *dbx.SelectQuery) error {
if requestInfo.Admin == nil && collection.ViewRule != nil && *collection.ViewRule != "" {
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestInfo, true)
2022-07-06 23:19:05 +02:00
expr, err := search.FilterData(*collection.ViewRule).BuildExpr(resolver)
if err != nil {
return err
}
resolver.UpdateQuery(q)
q.AndWhere(expr)
}
return nil
}
2022-10-30 10:28:14 +02:00
record, fetchErr := api.app.Dao().FindRecordById(collection.Id, recordId, ruleFunc)
2022-07-06 23:19:05 +02:00
if fetchErr != nil || record == nil {
2022-10-30 10:28:14 +02:00
return NewNotFoundError("", fetchErr)
2022-07-06 23:19:05 +02:00
}
event := new(core.RecordViewEvent)
event.HttpContext = c
event.Collection = collection
event.Record = record
2022-07-06 23:19:05 +02:00
return api.app.OnRecordViewRequest().Trigger(event, func(e *core.RecordViewEvent) error {
2023-07-20 09:40:03 +02:00
if e.HttpContext.Response().Committed {
return nil
}
if err := EnrichRecord(e.HttpContext, api.app.Dao(), e.Record); err != nil && api.app.IsDebug() {
log.Println(err)
}
2022-07-06 23:19:05 +02:00
return e.HttpContext.JSON(http.StatusOK, e.Record)
})
}
func (api *recordApi) create(c echo.Context) error {
collection, _ := c.Get(ContextCollectionKey).(*models.Collection)
if collection == nil {
2022-10-30 10:28:14 +02:00
return NewNotFoundError("", "Missing collection context.")
2022-07-06 23:19:05 +02:00
}
requestInfo := RequestInfo(c)
if requestInfo.Admin == nil && collection.CreateRule == nil {
2022-07-06 23:19:05 +02:00
// only admins can access if the rule is nil
2022-10-30 10:28:14 +02:00
return NewForbiddenError("Only admins can perform this action.", nil)
2022-07-06 23:19:05 +02:00
}
hasFullManageAccess := requestInfo.Admin != nil
2022-07-06 23:19:05 +02:00
// temporary save the record and check it against the create rule
if requestInfo.Admin == nil && collection.CreateRule != nil {
2023-01-07 22:25:56 +02:00
testRecord := models.NewRecord(collection)
// replace modifiers fields so that the resolved value is always
// available when accessing requestInfo.Data using just the field name
if requestInfo.HasModifierDataKeys() {
requestInfo.Data = testRecord.ReplaceModifers(requestInfo.Data)
2023-01-07 22:25:56 +02:00
}
testForm := forms.NewRecordUpsert(api.app, testRecord)
testForm.SetFullManageAccess(true)
if err := testForm.LoadRequest(c.Request(), ""); err != nil {
return NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
}
2022-10-30 10:28:14 +02:00
createRuleFunc := func(q *dbx.SelectQuery) error {
if *collection.CreateRule == "" {
return nil // no create rule to resolve
}
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestInfo, true)
2022-07-06 23:19:05 +02:00
expr, err := search.FilterData(*collection.CreateRule).BuildExpr(resolver)
if err != nil {
return err
}
resolver.UpdateQuery(q)
q.AndWhere(expr)
return nil
}
testErr := testForm.DrySubmit(func(txDao *daos.Dao) error {
2022-10-30 10:28:14 +02:00
foundRecord, err := txDao.FindRecordById(collection.Id, testRecord.Id, createRuleFunc)
if err != nil {
return fmt.Errorf("DrySubmit create rule failure: %w", err)
2022-10-30 10:28:14 +02:00
}
hasFullManageAccess = hasAuthManageAccess(txDao, foundRecord, requestInfo)
2022-10-30 10:28:14 +02:00
return nil
2022-07-06 23:19:05 +02:00
})
2022-10-30 10:28:14 +02:00
2022-07-06 23:19:05 +02:00
if testErr != nil {
return NewBadRequestError("Failed to create record.", testErr)
2022-07-06 23:19:05 +02:00
}
}
record := models.NewRecord(collection)
form := forms.NewRecordUpsert(api.app, record)
2022-10-30 10:28:14 +02:00
form.SetFullManageAccess(hasFullManageAccess)
2022-07-06 23:19:05 +02:00
// load request
2022-10-30 10:28:14 +02:00
if err := form.LoadRequest(c.Request(), ""); err != nil {
return NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
2022-07-06 23:19:05 +02:00
}
event := new(core.RecordCreateEvent)
event.HttpContext = c
event.Collection = collection
event.Record = record
event.UploadedFiles = form.FilesToUpload()
2022-07-06 23:19:05 +02:00
// create the record
2023-05-29 20:50:07 +02:00
return form.Submit(func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] {
return func(m *models.Record) error {
event.Record = m
return api.app.OnRecordBeforeCreateRequest().Trigger(event, func(e *core.RecordCreateEvent) error {
if err := next(e.Record); err != nil {
2022-10-30 10:28:14 +02:00
return NewBadRequestError("Failed to create record.", err)
}
2022-07-06 23:19:05 +02:00
if err := EnrichRecord(e.HttpContext, api.app.Dao(), e.Record); err != nil && api.app.IsDebug() {
log.Println(err)
2022-10-30 10:28:14 +02:00
}
2023-07-18 14:31:36 +02:00
return api.app.OnRecordAfterCreateRequest().Trigger(event, func(e *core.RecordCreateEvent) error {
2023-07-20 09:40:03 +02:00
if e.HttpContext.Response().Committed {
return nil
}
2023-07-18 14:31:36 +02:00
return e.HttpContext.JSON(http.StatusOK, e.Record)
})
})
}
2022-07-06 23:19:05 +02:00
})
}
func (api *recordApi) update(c echo.Context) error {
collection, _ := c.Get(ContextCollectionKey).(*models.Collection)
if collection == nil {
2022-10-30 10:28:14 +02:00
return NewNotFoundError("", "Missing collection context.")
2022-07-06 23:19:05 +02:00
}
recordId := c.PathParam("id")
if recordId == "" {
2022-10-30 10:28:14 +02:00
return NewNotFoundError("", nil)
2022-07-06 23:19:05 +02:00
}
requestInfo := RequestInfo(c)
if requestInfo.Admin == nil && collection.UpdateRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
2022-07-06 23:19:05 +02:00
2023-01-07 22:25:56 +02:00
// eager fetch the record so that the modifier field values are replaced
// and available when accessing requestInfo.Data using just the field name
if requestInfo.HasModifierDataKeys() {
2023-01-07 22:25:56 +02:00
record, err := api.app.Dao().FindRecordById(collection.Id, recordId)
if err != nil || record == nil {
return NewNotFoundError("", err)
}
requestInfo.Data = record.ReplaceModifers(requestInfo.Data)
2023-01-07 22:25:56 +02:00
}
2022-07-06 23:19:05 +02:00
ruleFunc := func(q *dbx.SelectQuery) error {
if requestInfo.Admin == nil && collection.UpdateRule != nil && *collection.UpdateRule != "" {
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestInfo, true)
2022-07-06 23:19:05 +02:00
expr, err := search.FilterData(*collection.UpdateRule).BuildExpr(resolver)
if err != nil {
return err
}
resolver.UpdateQuery(q)
q.AndWhere(expr)
}
return nil
}
// fetch record
2022-10-30 10:28:14 +02:00
record, fetchErr := api.app.Dao().FindRecordById(collection.Id, recordId, ruleFunc)
2022-07-06 23:19:05 +02:00
if fetchErr != nil || record == nil {
2022-10-30 10:28:14 +02:00
return NewNotFoundError("", fetchErr)
2022-07-06 23:19:05 +02:00
}
form := forms.NewRecordUpsert(api.app, record)
form.SetFullManageAccess(requestInfo.Admin != nil || hasAuthManageAccess(api.app.Dao(), record, requestInfo))
2022-07-06 23:19:05 +02:00
// load request
2022-10-30 10:28:14 +02:00
if err := form.LoadRequest(c.Request(), ""); err != nil {
return NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
2022-07-06 23:19:05 +02:00
}
event := new(core.RecordUpdateEvent)
event.HttpContext = c
event.Collection = collection
event.Record = record
event.UploadedFiles = form.FilesToUpload()
2022-07-06 23:19:05 +02:00
// update the record
2023-05-29 20:50:07 +02:00
return form.Submit(func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] {
return func(m *models.Record) error {
event.Record = m
return api.app.OnRecordBeforeUpdateRequest().Trigger(event, func(e *core.RecordUpdateEvent) error {
if err := next(e.Record); err != nil {
2022-10-30 10:28:14 +02:00
return NewBadRequestError("Failed to update record.", err)
}
2022-07-06 23:19:05 +02:00
if err := EnrichRecord(e.HttpContext, api.app.Dao(), e.Record); err != nil && api.app.IsDebug() {
log.Println(err)
2022-10-30 10:28:14 +02:00
}
2023-07-18 14:31:36 +02:00
return api.app.OnRecordAfterUpdateRequest().Trigger(event, func(e *core.RecordUpdateEvent) error {
2023-07-20 09:40:03 +02:00
if e.HttpContext.Response().Committed {
return nil
}
2023-07-18 14:31:36 +02:00
return e.HttpContext.JSON(http.StatusOK, e.Record)
})
})
}
2022-07-06 23:19:05 +02:00
})
}
func (api *recordApi) delete(c echo.Context) error {
collection, _ := c.Get(ContextCollectionKey).(*models.Collection)
if collection == nil {
2022-10-30 10:28:14 +02:00
return NewNotFoundError("", "Missing collection context.")
2022-07-06 23:19:05 +02:00
}
recordId := c.PathParam("id")
if recordId == "" {
2022-10-30 10:28:14 +02:00
return NewNotFoundError("", nil)
2022-07-06 23:19:05 +02:00
}
requestInfo := RequestInfo(c)
if requestInfo.Admin == nil && collection.DeleteRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
2022-07-06 23:19:05 +02:00
ruleFunc := func(q *dbx.SelectQuery) error {
if requestInfo.Admin == nil && collection.DeleteRule != nil && *collection.DeleteRule != "" {
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestInfo, true)
2022-07-06 23:19:05 +02:00
expr, err := search.FilterData(*collection.DeleteRule).BuildExpr(resolver)
if err != nil {
return err
}
resolver.UpdateQuery(q)
q.AndWhere(expr)
}
return nil
}
2022-10-30 10:28:14 +02:00
record, fetchErr := api.app.Dao().FindRecordById(collection.Id, recordId, ruleFunc)
2022-07-06 23:19:05 +02:00
if fetchErr != nil || record == nil {
2022-10-30 10:28:14 +02:00
return NewNotFoundError("", fetchErr)
2022-07-06 23:19:05 +02:00
}
event := new(core.RecordDeleteEvent)
event.HttpContext = c
event.Collection = collection
event.Record = record
2022-07-06 23:19:05 +02:00
2023-05-29 20:50:07 +02:00
return api.app.OnRecordBeforeDeleteRequest().Trigger(event, func(e *core.RecordDeleteEvent) error {
2022-07-06 23:19:05 +02:00
// delete the record
if err := api.app.Dao().DeleteRecord(e.Record); err != nil {
2022-10-30 10:28:14 +02:00
return NewBadRequestError("Failed to delete record. Make sure that the record is not part of a required relation reference.", err)
2022-07-06 23:19:05 +02:00
}
2023-07-18 14:31:36 +02:00
return api.app.OnRecordAfterDeleteRequest().Trigger(event, func(e *core.RecordDeleteEvent) error {
2023-07-20 09:40:03 +02:00
if e.HttpContext.Response().Committed {
return nil
}
2023-07-18 14:31:36 +02:00
return e.HttpContext.NoContent(http.StatusNoContent)
})
2023-05-29 20:50:07 +02:00
})
2022-07-06 23:19:05 +02:00
}
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) {
2022-10-30 10:28:14 +02:00
return NewForbiddenError("Only admins can filter by @collection and @request query params", nil)
}
}
return nil
}