mirror of
https://github.com/pocketbase/pocketbase.git
synced 2025-02-05 10:45:09 +02:00
953 lines
25 KiB
Go
953 lines
25 KiB
Go
package core
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/pocketbase/pocketbase/tools/dbutils"
|
|
"github.com/pocketbase/pocketbase/tools/hook"
|
|
"github.com/pocketbase/pocketbase/tools/security"
|
|
"github.com/pocketbase/pocketbase/tools/types"
|
|
"github.com/spf13/cast"
|
|
)
|
|
|
|
var (
|
|
_ Model = (*Collection)(nil)
|
|
_ DBExporter = (*Collection)(nil)
|
|
_ FilesManager = (*Collection)(nil)
|
|
)
|
|
|
|
const (
|
|
CollectionTypeBase = "base"
|
|
CollectionTypeAuth = "auth"
|
|
CollectionTypeView = "view"
|
|
)
|
|
|
|
const systemHookIdCollection = "__pbCollectionSystemHook__"
|
|
|
|
func (app *BaseApp) registerCollectionHooks() {
|
|
app.OnModelValidate().Bind(&hook.Handler[*ModelEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: func(me *ModelEvent) error {
|
|
if ce, ok := newCollectionEventFromModelEvent(me); ok {
|
|
return me.App.OnCollectionValidate().Trigger(ce, func(ce *CollectionEvent) error {
|
|
syncModelEventWithCollectionEvent(me, ce)
|
|
return me.Next()
|
|
})
|
|
}
|
|
|
|
return me.Next()
|
|
},
|
|
Priority: -99,
|
|
})
|
|
|
|
app.OnModelCreate().Bind(&hook.Handler[*ModelEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: func(me *ModelEvent) error {
|
|
if ce, ok := newCollectionEventFromModelEvent(me); ok {
|
|
return me.App.OnCollectionCreate().Trigger(ce, func(ce *CollectionEvent) error {
|
|
syncModelEventWithCollectionEvent(me, ce)
|
|
return me.Next()
|
|
})
|
|
}
|
|
|
|
return me.Next()
|
|
},
|
|
Priority: -99,
|
|
})
|
|
|
|
app.OnModelCreateExecute().Bind(&hook.Handler[*ModelEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: func(me *ModelEvent) error {
|
|
if ce, ok := newCollectionEventFromModelEvent(me); ok {
|
|
return me.App.OnCollectionCreateExecute().Trigger(ce, func(ce *CollectionEvent) error {
|
|
syncModelEventWithCollectionEvent(me, ce)
|
|
return me.Next()
|
|
})
|
|
}
|
|
|
|
return me.Next()
|
|
},
|
|
Priority: -99,
|
|
})
|
|
|
|
app.OnModelAfterCreateSuccess().Bind(&hook.Handler[*ModelEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: func(me *ModelEvent) error {
|
|
if ce, ok := newCollectionEventFromModelEvent(me); ok {
|
|
return me.App.OnCollectionAfterCreateSuccess().Trigger(ce, func(ce *CollectionEvent) error {
|
|
syncModelEventWithCollectionEvent(me, ce)
|
|
return me.Next()
|
|
})
|
|
}
|
|
|
|
return me.Next()
|
|
},
|
|
Priority: -99,
|
|
})
|
|
|
|
app.OnModelAfterCreateError().Bind(&hook.Handler[*ModelErrorEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: func(me *ModelErrorEvent) error {
|
|
if ce, ok := newCollectionErrorEventFromModelErrorEvent(me); ok {
|
|
return me.App.OnCollectionAfterCreateError().Trigger(ce, func(ce *CollectionErrorEvent) error {
|
|
syncModelErrorEventWithCollectionErrorEvent(me, ce)
|
|
return me.Next()
|
|
})
|
|
}
|
|
|
|
return me.Next()
|
|
},
|
|
Priority: -99,
|
|
})
|
|
|
|
app.OnModelUpdate().Bind(&hook.Handler[*ModelEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: func(me *ModelEvent) error {
|
|
if ce, ok := newCollectionEventFromModelEvent(me); ok {
|
|
return me.App.OnCollectionUpdate().Trigger(ce, func(ce *CollectionEvent) error {
|
|
syncModelEventWithCollectionEvent(me, ce)
|
|
return me.Next()
|
|
})
|
|
}
|
|
|
|
return me.Next()
|
|
},
|
|
Priority: -99,
|
|
})
|
|
|
|
app.OnModelUpdateExecute().Bind(&hook.Handler[*ModelEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: func(me *ModelEvent) error {
|
|
if ce, ok := newCollectionEventFromModelEvent(me); ok {
|
|
return me.App.OnCollectionUpdateExecute().Trigger(ce, func(ce *CollectionEvent) error {
|
|
syncModelEventWithCollectionEvent(me, ce)
|
|
return me.Next()
|
|
})
|
|
}
|
|
|
|
return me.Next()
|
|
},
|
|
Priority: -99,
|
|
})
|
|
|
|
app.OnModelAfterUpdateSuccess().Bind(&hook.Handler[*ModelEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: func(me *ModelEvent) error {
|
|
if ce, ok := newCollectionEventFromModelEvent(me); ok {
|
|
return me.App.OnCollectionAfterUpdateSuccess().Trigger(ce, func(ce *CollectionEvent) error {
|
|
syncModelEventWithCollectionEvent(me, ce)
|
|
return me.Next()
|
|
})
|
|
}
|
|
|
|
return me.Next()
|
|
},
|
|
Priority: -99,
|
|
})
|
|
|
|
app.OnModelAfterUpdateError().Bind(&hook.Handler[*ModelErrorEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: func(me *ModelErrorEvent) error {
|
|
if ce, ok := newCollectionErrorEventFromModelErrorEvent(me); ok {
|
|
return me.App.OnCollectionAfterUpdateError().Trigger(ce, func(ce *CollectionErrorEvent) error {
|
|
syncModelErrorEventWithCollectionErrorEvent(me, ce)
|
|
return me.Next()
|
|
})
|
|
}
|
|
|
|
return me.Next()
|
|
},
|
|
Priority: -99,
|
|
})
|
|
|
|
app.OnModelDelete().Bind(&hook.Handler[*ModelEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: func(me *ModelEvent) error {
|
|
if ce, ok := newCollectionEventFromModelEvent(me); ok {
|
|
return me.App.OnCollectionDelete().Trigger(ce, func(ce *CollectionEvent) error {
|
|
syncModelEventWithCollectionEvent(me, ce)
|
|
return me.Next()
|
|
})
|
|
}
|
|
|
|
return me.Next()
|
|
},
|
|
Priority: -99,
|
|
})
|
|
|
|
app.OnModelDeleteExecute().Bind(&hook.Handler[*ModelEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: func(me *ModelEvent) error {
|
|
if ce, ok := newCollectionEventFromModelEvent(me); ok {
|
|
return me.App.OnCollectionDeleteExecute().Trigger(ce, func(ce *CollectionEvent) error {
|
|
syncModelEventWithCollectionEvent(me, ce)
|
|
return me.Next()
|
|
})
|
|
}
|
|
|
|
return me.Next()
|
|
},
|
|
Priority: -99,
|
|
})
|
|
|
|
app.OnModelAfterDeleteSuccess().Bind(&hook.Handler[*ModelEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: func(me *ModelEvent) error {
|
|
if ce, ok := newCollectionEventFromModelEvent(me); ok {
|
|
return me.App.OnCollectionAfterDeleteSuccess().Trigger(ce, func(ce *CollectionEvent) error {
|
|
syncModelEventWithCollectionEvent(me, ce)
|
|
return me.Next()
|
|
})
|
|
}
|
|
|
|
return me.Next()
|
|
},
|
|
Priority: -99,
|
|
})
|
|
|
|
app.OnModelAfterDeleteError().Bind(&hook.Handler[*ModelErrorEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: func(me *ModelErrorEvent) error {
|
|
if ce, ok := newCollectionErrorEventFromModelErrorEvent(me); ok {
|
|
return me.App.OnCollectionAfterDeleteError().Trigger(ce, func(ce *CollectionErrorEvent) error {
|
|
syncModelErrorEventWithCollectionErrorEvent(me, ce)
|
|
return me.Next()
|
|
})
|
|
}
|
|
|
|
return me.Next()
|
|
},
|
|
Priority: -99,
|
|
})
|
|
|
|
// --------------------------------------------------------------
|
|
|
|
app.OnCollectionValidate().Bind(&hook.Handler[*CollectionEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: onCollectionValidate,
|
|
Priority: 99,
|
|
})
|
|
|
|
app.OnCollectionCreate().Bind(&hook.Handler[*CollectionEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: onCollectionSave,
|
|
Priority: -99,
|
|
})
|
|
|
|
app.OnCollectionUpdate().Bind(&hook.Handler[*CollectionEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: onCollectionSave,
|
|
Priority: -99,
|
|
})
|
|
|
|
app.OnCollectionCreateExecute().Bind(&hook.Handler[*CollectionEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: onCollectionSaveExecute,
|
|
// execute as latest as possible, aka. closer to the db action to minimize the transactions lock time
|
|
Priority: 99,
|
|
})
|
|
|
|
app.OnCollectionUpdateExecute().Bind(&hook.Handler[*CollectionEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: onCollectionSaveExecute,
|
|
Priority: 99, // execute as latest as possible, aka. closer to the db action to minimize the transactions lock time
|
|
})
|
|
|
|
app.OnCollectionDeleteExecute().Bind(&hook.Handler[*CollectionEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: onCollectionDeleteExecute,
|
|
Priority: 99, // execute as latest as possible, aka. closer to the db action to minimize the transactions lock time
|
|
})
|
|
|
|
// reload cache on failure
|
|
// ---
|
|
onErrorReloadCachedCollections := func(ce *CollectionErrorEvent) error {
|
|
if err := ce.App.ReloadCachedCollections(); err != nil {
|
|
ce.App.Logger().Warn("Failed to reload collections cache after collection change error", "error", err)
|
|
}
|
|
|
|
return ce.Next()
|
|
}
|
|
app.OnCollectionAfterCreateError().Bind(&hook.Handler[*CollectionErrorEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: onErrorReloadCachedCollections,
|
|
Priority: -99,
|
|
})
|
|
app.OnCollectionAfterUpdateError().Bind(&hook.Handler[*CollectionErrorEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: onErrorReloadCachedCollections,
|
|
Priority: -99,
|
|
})
|
|
app.OnCollectionAfterDeleteError().Bind(&hook.Handler[*CollectionErrorEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: onErrorReloadCachedCollections,
|
|
Priority: -99,
|
|
})
|
|
// ---
|
|
|
|
app.OnBootstrap().Bind(&hook.Handler[*BootstrapEvent]{
|
|
Id: systemHookIdCollection,
|
|
Func: func(e *BootstrapEvent) error {
|
|
if err := e.Next(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := e.App.ReloadCachedCollections(); err != nil {
|
|
return fmt.Errorf("failed to load collections cache: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Priority: 99, // execute as latest as possible
|
|
})
|
|
}
|
|
|
|
// @todo experiment eventually replacing the rules *string with a struct?
|
|
type baseCollection struct {
|
|
BaseModel
|
|
|
|
disableIntegrityChecks bool
|
|
|
|
ListRule *string `db:"listRule" json:"listRule" form:"listRule"`
|
|
ViewRule *string `db:"viewRule" json:"viewRule" form:"viewRule"`
|
|
CreateRule *string `db:"createRule" json:"createRule" form:"createRule"`
|
|
UpdateRule *string `db:"updateRule" json:"updateRule" form:"updateRule"`
|
|
DeleteRule *string `db:"deleteRule" json:"deleteRule" form:"deleteRule"`
|
|
|
|
// RawOptions represents the raw serialized collection option loaded from the DB.
|
|
// NB! This field shouldn't be modified manually. It is automatically updated
|
|
// with the collection type specific option before save.
|
|
RawOptions types.JSONRaw `db:"options" json:"-" xml:"-" form:"-"`
|
|
|
|
Name string `db:"name" json:"name" form:"name"`
|
|
Type string `db:"type" json:"type" form:"type"`
|
|
Fields FieldsList `db:"fields" json:"fields" form:"fields"`
|
|
Indexes types.JSONArray[string] `db:"indexes" json:"indexes" form:"indexes"`
|
|
Created types.DateTime `db:"created" json:"created"`
|
|
Updated types.DateTime `db:"updated" json:"updated"`
|
|
|
|
// System prevents the collection rename, deletion and rules change.
|
|
// It is used primarily for internal purposes for collections like "_superusers", "_externalAuths", etc.
|
|
System bool `db:"system" json:"system" form:"system"`
|
|
}
|
|
|
|
// Collection defines the table, fields and various options related to a set of records.
|
|
type Collection struct {
|
|
baseCollection
|
|
collectionAuthOptions
|
|
collectionViewOptions
|
|
}
|
|
|
|
// NewCollection initializes and returns a new Collection model with the specified type and name.
|
|
func NewCollection(typ, name string) *Collection {
|
|
switch typ {
|
|
case CollectionTypeAuth:
|
|
return NewAuthCollection(name)
|
|
case CollectionTypeView:
|
|
return NewViewCollection(name)
|
|
default:
|
|
return NewBaseCollection(name)
|
|
}
|
|
}
|
|
|
|
// NewBaseCollection initializes and returns a new "base" Collection model.
|
|
func NewBaseCollection(name string) *Collection {
|
|
m := &Collection{}
|
|
m.Name = name
|
|
m.Type = CollectionTypeBase
|
|
m.initDefaultId()
|
|
m.initDefaultFields()
|
|
return m
|
|
}
|
|
|
|
// NewViewCollection initializes and returns a new "view" Collection model.
|
|
func NewViewCollection(name string) *Collection {
|
|
m := &Collection{}
|
|
m.Name = name
|
|
m.Type = CollectionTypeView
|
|
m.initDefaultId()
|
|
m.initDefaultFields()
|
|
return m
|
|
}
|
|
|
|
// NewAuthCollection initializes and returns a new "auth" Collection model.
|
|
func NewAuthCollection(name string) *Collection {
|
|
m := &Collection{}
|
|
m.Name = name
|
|
m.Type = CollectionTypeAuth
|
|
m.initDefaultId()
|
|
m.initDefaultFields()
|
|
m.setDefaultAuthOptions()
|
|
return m
|
|
}
|
|
|
|
// TableName returns the Collection model SQL table name.
|
|
func (m *Collection) TableName() string {
|
|
return "_collections"
|
|
}
|
|
|
|
// BaseFilesPath returns the storage dir path used by the collection.
|
|
func (m *Collection) BaseFilesPath() string {
|
|
return m.Id
|
|
}
|
|
|
|
// IsBase checks if the current collection has "base" type.
|
|
func (m *Collection) IsBase() bool {
|
|
return m.Type == CollectionTypeBase
|
|
}
|
|
|
|
// IsAuth checks if the current collection has "auth" type.
|
|
func (m *Collection) IsAuth() bool {
|
|
return m.Type == CollectionTypeAuth
|
|
}
|
|
|
|
// IsView checks if the current collection has "view" type.
|
|
func (m *Collection) IsView() bool {
|
|
return m.Type == CollectionTypeView
|
|
}
|
|
|
|
// IntegrityChecks toggles the current collection integrity checks (ex. checking references on delete).
|
|
func (m *Collection) IntegrityChecks(enable bool) {
|
|
m.disableIntegrityChecks = !enable
|
|
}
|
|
|
|
// PostScan implements the [dbx.PostScanner] interface to auto unmarshal
|
|
// the raw serialized options into the concrete type specific fields.
|
|
func (m *Collection) PostScan() error {
|
|
if err := m.BaseModel.PostScan(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return m.unmarshalRawOptions()
|
|
}
|
|
|
|
func (m *Collection) unmarshalRawOptions() error {
|
|
raw, err := m.RawOptions.MarshalJSON()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
switch m.Type {
|
|
case CollectionTypeView:
|
|
return json.Unmarshal(raw, &m.collectionViewOptions)
|
|
case CollectionTypeAuth:
|
|
return json.Unmarshal(raw, &m.collectionAuthOptions)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UnmarshalJSON implements the [json.Unmarshaler] interface.
|
|
//
|
|
// For new/"blank" Collection models it replaces the model with a factory
|
|
// instance and then unmarshal the provided data one on top of it.
|
|
func (m *Collection) UnmarshalJSON(b []byte) error {
|
|
type alias *Collection
|
|
|
|
// initialize the default fields
|
|
// (e.g. in case the collection was NOT created using the designated factories)
|
|
if m.IsNew() && m.Type == "" {
|
|
minimal := &struct {
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
}{}
|
|
if err := json.Unmarshal(b, minimal); err != nil {
|
|
return err
|
|
}
|
|
|
|
blank := NewCollection(minimal.Type, minimal.Name)
|
|
*m = *blank
|
|
}
|
|
|
|
return json.Unmarshal(b, alias(m))
|
|
}
|
|
|
|
// MarshalJSON implements the [json.Marshaler] interface.
|
|
//
|
|
// Note that non-type related fields are ignored from the serialization
|
|
// (ex. for "view" colections the "auth" fields are skipped).
|
|
func (m Collection) MarshalJSON() ([]byte, error) {
|
|
switch m.Type {
|
|
case CollectionTypeView:
|
|
return json.Marshal(struct {
|
|
baseCollection
|
|
collectionViewOptions
|
|
}{m.baseCollection, m.collectionViewOptions})
|
|
case CollectionTypeAuth:
|
|
alias := struct {
|
|
baseCollection
|
|
collectionAuthOptions
|
|
}{m.baseCollection, m.collectionAuthOptions}
|
|
|
|
// ensure that it is always returned as array
|
|
if alias.OAuth2.Providers == nil {
|
|
alias.OAuth2.Providers = []OAuth2ProviderConfig{}
|
|
}
|
|
|
|
// hide secret keys from the serialization
|
|
alias.AuthToken.Secret = ""
|
|
alias.FileToken.Secret = ""
|
|
alias.PasswordResetToken.Secret = ""
|
|
alias.EmailChangeToken.Secret = ""
|
|
alias.VerificationToken.Secret = ""
|
|
for i := range alias.OAuth2.Providers {
|
|
alias.OAuth2.Providers[i].ClientSecret = ""
|
|
}
|
|
|
|
return json.Marshal(alias)
|
|
default:
|
|
return json.Marshal(m.baseCollection)
|
|
}
|
|
}
|
|
|
|
// String returns a string representation of the current collection.
|
|
func (m Collection) String() string {
|
|
raw, _ := json.Marshal(m)
|
|
return string(raw)
|
|
}
|
|
|
|
// DBExport prepares and exports the current collection data for db persistence.
|
|
func (m *Collection) DBExport(app App) (map[string]any, error) {
|
|
result := map[string]any{
|
|
"id": m.Id,
|
|
"type": m.Type,
|
|
"listRule": m.ListRule,
|
|
"viewRule": m.ViewRule,
|
|
"createRule": m.CreateRule,
|
|
"updateRule": m.UpdateRule,
|
|
"deleteRule": m.DeleteRule,
|
|
"name": m.Name,
|
|
"fields": m.Fields,
|
|
"indexes": m.Indexes,
|
|
"system": m.System,
|
|
"created": m.Created,
|
|
"updated": m.Updated,
|
|
"options": `{}`,
|
|
}
|
|
|
|
switch m.Type {
|
|
case CollectionTypeView:
|
|
if raw, err := types.ParseJSONRaw(m.collectionViewOptions); err == nil {
|
|
result["options"] = raw
|
|
} else {
|
|
return nil, err
|
|
}
|
|
case CollectionTypeAuth:
|
|
if raw, err := types.ParseJSONRaw(m.collectionAuthOptions); err == nil {
|
|
result["options"] = raw
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// GetIndex returns s single Collection index expression by its name.
|
|
func (m *Collection) GetIndex(name string) string {
|
|
for _, idx := range m.Indexes {
|
|
if strings.EqualFold(dbutils.ParseIndex(idx).IndexName, name) {
|
|
return idx
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// AddIndex adds a new index into the current collection.
|
|
//
|
|
// If the collection has an existing index matching the new name it will be replaced with the new one.
|
|
func (m *Collection) AddIndex(name string, unique bool, columnsExpr string, optWhereExpr string) {
|
|
m.RemoveIndex(name)
|
|
|
|
var idx strings.Builder
|
|
|
|
idx.WriteString("CREATE ")
|
|
if unique {
|
|
idx.WriteString("UNIQUE ")
|
|
}
|
|
idx.WriteString("INDEX `")
|
|
idx.WriteString(name)
|
|
idx.WriteString("` ")
|
|
idx.WriteString("ON `")
|
|
idx.WriteString(m.Name)
|
|
idx.WriteString("` (")
|
|
idx.WriteString(columnsExpr)
|
|
idx.WriteString(")")
|
|
if optWhereExpr != "" {
|
|
idx.WriteString(" WHERE ")
|
|
idx.WriteString(optWhereExpr)
|
|
}
|
|
|
|
m.Indexes = append(m.Indexes, idx.String())
|
|
}
|
|
|
|
// RemoveIndex removes a single index with the specified name from the current collection.
|
|
func (m *Collection) RemoveIndex(name string) {
|
|
for i, idx := range m.Indexes {
|
|
if strings.EqualFold(dbutils.ParseIndex(idx).IndexName, name) {
|
|
m.Indexes = append(m.Indexes[:i], m.Indexes[i+1:]...)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// delete hook
|
|
// -------------------------------------------------------------------
|
|
|
|
func onCollectionDeleteExecute(e *CollectionEvent) error {
|
|
if e.Collection.System {
|
|
return fmt.Errorf("[%s] system collections cannot be deleted", e.Collection.Name)
|
|
}
|
|
|
|
defer func() {
|
|
if err := e.App.ReloadCachedCollections(); err != nil {
|
|
e.App.Logger().Warn("Failed to reload collections cache", "error", err)
|
|
}
|
|
}()
|
|
|
|
if !e.Collection.disableIntegrityChecks {
|
|
// ensure that there aren't any existing references.
|
|
// note: the select is outside of the transaction to prevent SQLITE_LOCKED error when mixing read&write in a single transaction
|
|
references, err := e.App.FindCollectionReferences(e.Collection, e.Collection.Id)
|
|
if err != nil {
|
|
return fmt.Errorf("[%s] failed to check collection references: %w", e.Collection.Name, err)
|
|
}
|
|
if total := len(references); total > 0 {
|
|
names := make([]string, 0, len(references))
|
|
for ref := range references {
|
|
names = append(names, ref.Name)
|
|
}
|
|
return fmt.Errorf("[%s] failed to delete due to existing relation references: %s", e.Collection.Name, strings.Join(names, ", "))
|
|
}
|
|
}
|
|
|
|
originalApp := e.App
|
|
|
|
txErr := e.App.RunInTransaction(func(txApp App) error {
|
|
e.App = txApp
|
|
|
|
// delete the related view or records table
|
|
if e.Collection.IsView() {
|
|
if err := txApp.DeleteView(e.Collection.Name); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := txApp.DeleteTable(e.Collection.Name); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if !e.Collection.disableIntegrityChecks {
|
|
// trigger views resave to check for dependencies
|
|
if err := resaveViewsWithChangedFields(txApp, e.Collection.Id); err != nil {
|
|
return fmt.Errorf("[%s] failed to delete due to existing view dependency: %w", e.Collection.Name, err)
|
|
}
|
|
}
|
|
|
|
// delete
|
|
return e.Next()
|
|
})
|
|
|
|
e.App = originalApp
|
|
|
|
return txErr
|
|
}
|
|
|
|
// save hook
|
|
// -------------------------------------------------------------------
|
|
|
|
func (c *Collection) initDefaultId() {
|
|
if c.Id == "" && c.Name != "" {
|
|
c.Id = "_pbc_" + crc32Checksum(c.Name)
|
|
}
|
|
}
|
|
|
|
func (c *Collection) savePrepare() error {
|
|
if c.Type == "" {
|
|
c.Type = CollectionTypeBase
|
|
}
|
|
|
|
if c.IsNew() {
|
|
c.initDefaultId()
|
|
c.Created = types.NowDateTime()
|
|
}
|
|
|
|
c.Updated = types.NowDateTime()
|
|
|
|
// recreate the fields list to ensure that all normalizations
|
|
// like default field id are applied
|
|
c.Fields = NewFieldsList(c.Fields...)
|
|
|
|
c.initDefaultFields()
|
|
|
|
if c.IsAuth() {
|
|
c.unsetMissingOAuth2MappedFields()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func onCollectionSave(e *CollectionEvent) error {
|
|
if err := e.Collection.savePrepare(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return e.Next()
|
|
}
|
|
|
|
func onCollectionSaveExecute(e *CollectionEvent) error {
|
|
defer func() {
|
|
if err := e.App.ReloadCachedCollections(); err != nil {
|
|
e.App.Logger().Warn("Failed to reload collections cache", "error", err)
|
|
}
|
|
}()
|
|
|
|
var oldCollection *Collection
|
|
if !e.Collection.IsNew() {
|
|
var err error
|
|
oldCollection, err = e.App.FindCachedCollectionByNameOrId(e.Collection.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// invalidate previously issued auth tokens on auth rule change
|
|
if oldCollection.AuthRule != e.Collection.AuthRule &&
|
|
cast.ToString(oldCollection.AuthRule) != cast.ToString(e.Collection.AuthRule) {
|
|
e.Collection.AuthToken.Secret = security.RandomString(50)
|
|
}
|
|
}
|
|
|
|
originalApp := e.App
|
|
txErr := e.App.RunInTransaction(func(txApp App) error {
|
|
e.App = txApp
|
|
|
|
isView := e.Collection.IsView()
|
|
|
|
// ensures that the view collection shema is properly loaded
|
|
if isView {
|
|
query := e.Collection.ViewQuery
|
|
|
|
// generate collection fields list from the query
|
|
viewFields, err := e.App.CreateViewFields(query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// delete old renamed view
|
|
if oldCollection != nil {
|
|
if err := e.App.DeleteView(oldCollection.Name); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// wrap view query if necessary
|
|
query, err = normalizeViewQueryId(e.App, query)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to normalize view query id: %w", err)
|
|
}
|
|
|
|
// (re)create the view
|
|
if err := e.App.SaveView(e.Collection.Name, query); err != nil {
|
|
return err
|
|
}
|
|
|
|
// updates newCollection.Fields based on the generated view table info and query
|
|
e.Collection.Fields = viewFields
|
|
}
|
|
|
|
// save the Collection model
|
|
if err := e.Next(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// sync the changes with the related records table
|
|
if !isView {
|
|
if err := e.App.SyncRecordTableSchema(e.Collection, oldCollection); err != nil {
|
|
// note: don't wrap to allow propagating indexes validation.Errors
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
e.App = originalApp
|
|
|
|
if txErr != nil {
|
|
return txErr
|
|
}
|
|
|
|
// trigger an update for all views with changed fields as a result of the current collection save
|
|
// (ignoring view errors to allow users to update the query from the UI)
|
|
resaveViewsWithChangedFields(e.App, e.Collection.Id)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Collection) initDefaultFields() {
|
|
switch m.Type {
|
|
case CollectionTypeBase:
|
|
m.initIdField()
|
|
case CollectionTypeAuth:
|
|
m.initIdField()
|
|
m.initPasswordField()
|
|
m.initTokenKeyField()
|
|
m.initEmailField()
|
|
m.initEmailVisibilityField()
|
|
m.initVerifiedField()
|
|
case CollectionTypeView:
|
|
// view fields are autogenerated
|
|
}
|
|
}
|
|
|
|
func (m *Collection) initIdField() {
|
|
field, _ := m.Fields.GetByName(FieldNameId).(*TextField)
|
|
if field == nil {
|
|
// create default field
|
|
field = &TextField{
|
|
Name: FieldNameId,
|
|
System: true,
|
|
PrimaryKey: true,
|
|
Required: true,
|
|
Min: 15,
|
|
Max: 15,
|
|
Pattern: `^[a-z0-9]+$`,
|
|
AutogeneratePattern: `[a-z0-9]{15}`,
|
|
}
|
|
|
|
// prepend it
|
|
m.Fields = NewFieldsList(append([]Field{field}, m.Fields...)...)
|
|
} else {
|
|
// enforce system defaults
|
|
field.System = true
|
|
field.Required = true
|
|
field.PrimaryKey = true
|
|
field.Hidden = false
|
|
}
|
|
}
|
|
|
|
func (m *Collection) initPasswordField() {
|
|
field, _ := m.Fields.GetByName(FieldNamePassword).(*PasswordField)
|
|
if field == nil {
|
|
// load default field
|
|
m.Fields.Add(&PasswordField{
|
|
Name: FieldNamePassword,
|
|
System: true,
|
|
Hidden: true,
|
|
Required: true,
|
|
Min: 8,
|
|
})
|
|
} else {
|
|
// enforce system defaults
|
|
field.System = true
|
|
field.Hidden = true
|
|
field.Required = true
|
|
}
|
|
}
|
|
|
|
func (m *Collection) initTokenKeyField() {
|
|
field, _ := m.Fields.GetByName(FieldNameTokenKey).(*TextField)
|
|
if field == nil {
|
|
// load default field
|
|
m.Fields.Add(&TextField{
|
|
Name: FieldNameTokenKey,
|
|
System: true,
|
|
Hidden: true,
|
|
Min: 30,
|
|
Max: 60,
|
|
Required: true,
|
|
AutogeneratePattern: `[a-zA-Z0-9]{50}`,
|
|
})
|
|
} else {
|
|
// enforce system defaults
|
|
field.System = true
|
|
field.Hidden = true
|
|
field.Required = true
|
|
}
|
|
|
|
// ensure that there is a unique index for the field
|
|
if !dbutils.HasSingleColumnUniqueIndex(FieldNameTokenKey, m.Indexes) {
|
|
m.Indexes = append(m.Indexes, fmt.Sprintf(
|
|
"CREATE UNIQUE INDEX `%s` ON `%s` (`%s`)",
|
|
m.fieldIndexName(FieldNameTokenKey),
|
|
m.Name,
|
|
FieldNameTokenKey,
|
|
))
|
|
}
|
|
}
|
|
|
|
func (m *Collection) initEmailField() {
|
|
field, _ := m.Fields.GetByName(FieldNameEmail).(*EmailField)
|
|
if field == nil {
|
|
// load default field
|
|
m.Fields.Add(&EmailField{
|
|
Name: FieldNameEmail,
|
|
System: true,
|
|
Required: true,
|
|
})
|
|
} else {
|
|
// enforce system defaults
|
|
field.System = true
|
|
field.Hidden = false // managed by the emailVisibility flag
|
|
}
|
|
|
|
// ensure that there is a unique index for the email field
|
|
if !dbutils.HasSingleColumnUniqueIndex(FieldNameEmail, m.Indexes) {
|
|
m.Indexes = append(m.Indexes, fmt.Sprintf(
|
|
"CREATE UNIQUE INDEX `%s` ON `%s` (`%s`) WHERE `%s` != ''",
|
|
m.fieldIndexName(FieldNameEmail),
|
|
m.Name,
|
|
FieldNameEmail,
|
|
FieldNameEmail,
|
|
))
|
|
}
|
|
}
|
|
|
|
func (m *Collection) initEmailVisibilityField() {
|
|
field, _ := m.Fields.GetByName(FieldNameEmailVisibility).(*BoolField)
|
|
if field == nil {
|
|
// load default field
|
|
m.Fields.Add(&BoolField{
|
|
Name: FieldNameEmailVisibility,
|
|
System: true,
|
|
})
|
|
} else {
|
|
// enforce system defaults
|
|
field.System = true
|
|
}
|
|
}
|
|
|
|
func (m *Collection) initVerifiedField() {
|
|
field, _ := m.Fields.GetByName(FieldNameVerified).(*BoolField)
|
|
if field == nil {
|
|
// load default field
|
|
m.Fields.Add(&BoolField{
|
|
Name: FieldNameVerified,
|
|
System: true,
|
|
})
|
|
} else {
|
|
// enforce system defaults
|
|
field.System = true
|
|
}
|
|
}
|
|
|
|
func (m *Collection) fieldIndexName(field string) string {
|
|
name := "idx_" + field + "_"
|
|
|
|
if m.Id != "" {
|
|
name += m.Id
|
|
} else if m.Name != "" {
|
|
name += m.Name
|
|
} else {
|
|
name += security.PseudorandomString(10)
|
|
}
|
|
|
|
if len(name) > 64 {
|
|
return name[:64]
|
|
}
|
|
|
|
return name
|
|
}
|