diff --git a/core/collection_model.go b/core/collection_model.go index 6707a502..fe04e77c 100644 --- a/core/collection_model.go +++ b/core/collection_model.go @@ -3,8 +3,10 @@ package core import ( "encoding/json" "fmt" + "strconv" "strings" + "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/tools/dbutils" "github.com/pocketbase/pocketbase/tools/hook" "github.com/pocketbase/pocketbase/tools/security" @@ -309,6 +311,7 @@ type baseCollection struct { BaseModel disableIntegrityChecks bool + autogeneratedId string ListRule *string `db:"listRule" json:"listRule" form:"listRule"` ViewRule *string `db:"viewRule" json:"viewRule" form:"viewRule"` @@ -364,6 +367,7 @@ func NewBaseCollection(name string, optId ...string) *Collection { m.Name = name m.Type = CollectionTypeBase + // @todo consider removing once inferred composite literals are supported if len(optId) > 0 { m.Id = optId[0] } @@ -383,6 +387,7 @@ func NewViewCollection(name string, optId ...string) *Collection { m.Name = name m.Type = CollectionTypeView + // @todo consider removing once inferred composite literals are supported if len(optId) > 0 { m.Id = optId[0] } @@ -402,6 +407,7 @@ func NewAuthCollection(name string, optId ...string) *Collection { m.Name = name m.Type = CollectionTypeAuth + // @todo consider removing once inferred composite literals are supported if len(optId) > 0 { m.Id = optId[0] } @@ -689,49 +695,82 @@ func onCollectionDeleteExecute(e *CollectionEvent) error { // save hook // ------------------------------------------------------------------- -func (c *Collection) initDefaultId() { - if c.Id != "" { - return // already set - } +func (c *Collection) idChecksum() string { + return "pbc_" + crc32Checksum(c.Type+c.Name) +} - if c.System && c.Name != "" { - // for system collections we use crc32 checksum for consistency because they cannot be renamed - c.Id = "pbc_" + crc32Checksum(c.Name) - } else { - c.Id = "pbc_" + security.RandomStringWithAlphabet(11, DefaultIdAlphabet) +func (c *Collection) initDefaultId() { + if c.Id == "" { + c.Id = c.idChecksum() + c.autogeneratedId = c.Id } } -func (c *Collection) savePrepare() error { - if c.Type == "" { - c.Type = CollectionTypeBase +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 } - if c.IsNew() { - c.initDefaultId() - c.Created = types.NowDateTime() + // 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) } - 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() + // no change + if c.Id == newId { + return } - return nil + // 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() + } + } + + // update model id + c.Id = newId } func onCollectionSave(e *CollectionEvent) error { - if err := e.Collection.savePrepare(); err != nil { - return err + if e.Collection.Type == "" { + e.Collection.Type = CollectionTypeBase } + if e.Collection.IsNew() { + e.Collection.initDefaultId() + e.Collection.Created = types.NowDateTime() + } + + e.Collection.Updated = types.NowDateTime() + + // recreate the fields list to ensure that all normalizations + // like default field id are applied + e.Collection.Fields = NewFieldsList(e.Collection.Fields...) + + e.Collection.initDefaultFields() + + if e.Collection.IsAuth() { + e.Collection.unsetMissingOAuth2MappedFields() + } + + e.Collection.updateGeneratedIdIfExists(e.App) + return e.Next() } diff --git a/core/collection_validate.go b/core/collection_validate.go index 9699d089..875871bd 100644 --- a/core/collection_validate.go +++ b/core/collection_validate.go @@ -65,6 +65,10 @@ type optionsValidator interface { } func (validator *collectionValidator) run() error { + if validator.original.IsNew() { + validator.new.updateGeneratedIdIfExists(validator.app) + } + // generate fields from the query (overwriting any explicit user defined fields) if validator.new.IsView() { validator.new.Fields, _ = validator.app.CreateViewFields(validator.new.ViewQuery) diff --git a/core/fields_list.go b/core/fields_list.go index 5e640d8f..4f91c435 100644 --- a/core/fields_list.go +++ b/core/fields_list.go @@ -4,8 +4,7 @@ import ( "database/sql/driver" "encoding/json" "fmt" - - "github.com/pocketbase/pocketbase/tools/security" + "strconv" ) // NewFieldsList creates a new FieldsList instance with the provided fields. @@ -171,12 +170,16 @@ func (l *FieldsList) add(newField Field) { // set default id if newFieldId == "" { replaceByName = true - if newField.GetSystem() && newField.GetName() != "" { - // for system fields we use crc32 checksum for consistency because they cannot be renamed - newFieldId = newField.Type() + crc32Checksum(newField.GetName()) - } else { - newFieldId = newField.Type() + security.RandomString(7) + + baseId := newField.Type() + crc32Checksum(newField.GetName()) + newFieldId = baseId + for i := 2; i < 1000; i++ { + if l.GetById(newFieldId) == nil { + break // already unique + } + newFieldId = baseId + strconv.Itoa(i) } + newField.SetId(newFieldId) } // replace existing @@ -196,7 +199,6 @@ func (l *FieldsList) add(newField Field) { } // add new field - newField.SetId(newFieldId) *l = append(fields, newField) }