1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-03-19 14:17:48 +02:00
pocketbase/core/collection_model.go

1029 lines
27 KiB
Go
Raw Normal View History

2024-09-29 19:23:19 +03:00
package core
import (
"encoding/json"
"fmt"
"strconv"
2024-09-29 19:23:19 +03:00
"strings"
"github.com/pocketbase/dbx"
2024-09-29 19:23:19 +03:00
"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 {
2024-10-18 07:55:25 +03:00
ce.App.Logger().Warn("Failed to reload collections cache after collection change error", "error", err)
2024-09-29 19:23:19 +03:00
}
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
autogeneratedId string
2024-09-29 19:23:19 +03:00
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"`
2024-09-29 19:23:19 +03:00
}
// 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.
//
// It also loads the minimal default configuration for the collection
// (eg. system fields, indexes, type specific options, etc.).
func NewCollection(typ, name string, optId ...string) *Collection {
2024-09-29 19:23:19 +03:00
switch typ {
case CollectionTypeAuth:
return NewAuthCollection(name, optId...)
2024-09-29 19:23:19 +03:00
case CollectionTypeView:
return NewViewCollection(name, optId...)
2024-09-29 19:23:19 +03:00
default:
return NewBaseCollection(name, optId...)
2024-09-29 19:23:19 +03:00
}
}
// NewBaseCollection initializes and returns a new "base" Collection model.
//
// It also loads the minimal default configuration for the collection
// (eg. system fields, indexes, type specific options, etc.).
func NewBaseCollection(name string, optId ...string) *Collection {
2024-09-29 19:23:19 +03:00
m := &Collection{}
m.Name = name
m.Type = CollectionTypeBase
// @todo consider removing once inferred composite literals are supported
if len(optId) > 0 {
m.Id = optId[0]
}
2024-09-29 19:23:19 +03:00
m.initDefaultId()
m.initDefaultFields()
2024-09-29 19:23:19 +03:00
return m
}
// NewViewCollection initializes and returns a new "view" Collection model.
//
// It also loads the minimal default configuration for the collection
// (eg. system fields, indexes, type specific options, etc.).
func NewViewCollection(name string, optId ...string) *Collection {
2024-09-29 19:23:19 +03:00
m := &Collection{}
m.Name = name
m.Type = CollectionTypeView
// @todo consider removing once inferred composite literals are supported
if len(optId) > 0 {
m.Id = optId[0]
}
2024-09-29 19:23:19 +03:00
m.initDefaultId()
m.initDefaultFields()
2024-09-29 19:23:19 +03:00
return m
}
// NewAuthCollection initializes and returns a new "auth" Collection model.
//
// It also loads the minimal default configuration for the collection
// (eg. system fields, indexes, type specific options, etc.).
func NewAuthCollection(name string, optId ...string) *Collection {
2024-09-29 19:23:19 +03:00
m := &Collection{}
m.Name = name
m.Type = CollectionTypeAuth
// @todo consider removing once inferred composite literals are supported
if len(optId) > 0 {
m.Id = optId[0]
}
2024-09-29 19:23:19 +03:00
m.initDefaultId()
m.initDefaultFields()
m.setDefaultAuthOptions()
2024-09-29 19:23:19 +03:00
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) idChecksum() string {
return "pbc_" + crc32Checksum(c.Type+c.Name)
}
2024-09-29 19:23:19 +03:00
func (c *Collection) initDefaultId() {
if c.Id == "" {
c.Id = c.idChecksum()
c.autogeneratedId = c.Id
}
}
func (c *Collection) updateGeneratedIdIfExists(app App) {
if !c.IsNew() ||
// the id was explicitly cleared
c.Id == "" ||
// the id was manually set
c.Id != c.autogeneratedId {
return
}
// generate an up-to-date checksum
newId := c.idChecksum()
// add a number to the current id (if already exists)
for i := 2; i < 1000; i++ {
var exists bool
_ = app.CollectionQuery().Select("(1)").AndWhere(dbx.HashExp{"id": newId}).Limit(1).Row(&exists)
if !exists {
break
}
newId = c.idChecksum() + strconv.Itoa(i)
}
// no change
if c.Id == newId {
return
}
// replace the old id in the index names (if any)
for i, idx := range c.Indexes {
parsed := dbutils.ParseIndex(idx)
original := parsed.IndexName
parsed.IndexName = strings.ReplaceAll(parsed.IndexName, c.Id, newId)
if parsed.IndexName != original {
c.Indexes[i] = parsed.Build()
}
2024-09-29 19:23:19 +03:00
}
// update model id
c.Id = newId
2024-09-29 19:23:19 +03:00
}
func onCollectionSave(e *CollectionEvent) error {
if e.Collection.Type == "" {
e.Collection.Type = CollectionTypeBase
2024-09-29 19:23:19 +03:00
}
if e.Collection.IsNew() {
e.Collection.initDefaultId()
e.Collection.Created = types.NowDateTime()
2024-09-29 19:23:19 +03:00
}
e.Collection.Updated = types.NowDateTime()
2024-09-29 19:23:19 +03:00
// recreate the fields list to ensure that all normalizations
// like default field id are applied
e.Collection.Fields = NewFieldsList(e.Collection.Fields...)
2024-09-29 19:23:19 +03:00
e.Collection.initDefaultFields()
2024-09-29 19:23:19 +03:00
if e.Collection.IsAuth() {
e.Collection.unsetMissingOAuth2MappedFields()
2024-09-29 19:23:19 +03:00
}
e.Collection.updateGeneratedIdIfExists(e.App)
2024-09-29 19:23:19 +03:00
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 (c *Collection) initDefaultFields() {
switch c.Type {
2024-09-29 19:23:19 +03:00
case CollectionTypeBase:
c.initIdField()
2024-09-29 19:23:19 +03:00
case CollectionTypeAuth:
c.initIdField()
c.initPasswordField()
c.initTokenKeyField()
c.initEmailField()
c.initEmailVisibilityField()
c.initVerifiedField()
2024-09-29 19:23:19 +03:00
case CollectionTypeView:
// view fields are autogenerated
}
}
func (c *Collection) initIdField() {
field, _ := c.Fields.GetByName(FieldNameId).(*TextField)
2024-09-29 19:23:19 +03:00
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
c.Fields = NewFieldsList(append([]Field{field}, c.Fields...)...)
2024-09-29 19:23:19 +03:00
} else {
// enforce system defaults
field.System = true
field.Required = true
field.PrimaryKey = true
field.Hidden = false
}
}
func (c *Collection) initPasswordField() {
field, _ := c.Fields.GetByName(FieldNamePassword).(*PasswordField)
2024-09-29 19:23:19 +03:00
if field == nil {
// load default field
c.Fields.Add(&PasswordField{
2024-09-29 19:23:19 +03:00
Name: FieldNamePassword,
System: true,
Hidden: true,
Required: true,
Min: 8,
})
} else {
// enforce system defaults
field.System = true
field.Hidden = true
field.Required = true
}
}
func (c *Collection) initTokenKeyField() {
field, _ := c.Fields.GetByName(FieldNameTokenKey).(*TextField)
2024-09-29 19:23:19 +03:00
if field == nil {
// load default field
c.Fields.Add(&TextField{
2024-09-29 19:23:19 +03:00
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, c.Indexes) {
c.Indexes = append(c.Indexes, fmt.Sprintf(
2024-09-29 19:23:19 +03:00
"CREATE UNIQUE INDEX `%s` ON `%s` (`%s`)",
c.fieldIndexName(FieldNameTokenKey),
c.Name,
2024-09-29 19:23:19 +03:00
FieldNameTokenKey,
))
}
}
func (c *Collection) initEmailField() {
field, _ := c.Fields.GetByName(FieldNameEmail).(*EmailField)
2024-09-29 19:23:19 +03:00
if field == nil {
// load default field
c.Fields.Add(&EmailField{
2024-09-29 19:23:19 +03:00
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, c.Indexes) {
c.Indexes = append(c.Indexes, fmt.Sprintf(
2024-09-29 19:23:19 +03:00
"CREATE UNIQUE INDEX `%s` ON `%s` (`%s`) WHERE `%s` != ''",
c.fieldIndexName(FieldNameEmail),
c.Name,
2024-09-29 19:23:19 +03:00
FieldNameEmail,
FieldNameEmail,
))
}
}
func (c *Collection) initEmailVisibilityField() {
field, _ := c.Fields.GetByName(FieldNameEmailVisibility).(*BoolField)
2024-09-29 19:23:19 +03:00
if field == nil {
// load default field
c.Fields.Add(&BoolField{
2024-09-29 19:23:19 +03:00
Name: FieldNameEmailVisibility,
System: true,
})
} else {
// enforce system defaults
field.System = true
}
}
func (c *Collection) initVerifiedField() {
field, _ := c.Fields.GetByName(FieldNameVerified).(*BoolField)
2024-09-29 19:23:19 +03:00
if field == nil {
// load default field
c.Fields.Add(&BoolField{
2024-09-29 19:23:19 +03:00
Name: FieldNameVerified,
System: true,
})
} else {
// enforce system defaults
field.System = true
}
}
func (c *Collection) fieldIndexName(field string) string {
2024-09-29 19:23:19 +03:00
name := "idx_" + field + "_"
if c.Id != "" {
name += c.Id
} else if c.Name != "" {
name += c.Name
2024-09-29 19:23:19 +03:00
} else {
name += security.PseudorandomString(10)
}
if len(name) > 64 {
return name[:64]
}
return name
}