From 5fb45a186488bf261d761633685adf10a728c0b6 Mon Sep 17 00:00:00 2001 From: Gani Georgiev Date: Sat, 6 Aug 2022 18:15:18 +0300 Subject: [PATCH] updated CollectionsImport and CollectionUpsert forms --- daos/base.go | 5 +- forms/collection_upsert.go | 86 +++++++++++------ forms/collections_import.go | 61 ++++++++---- models/base.go | 5 +- ui/package.json | 1 - .../components/admins/AdminUpsertPanel.svelte | 5 +- ui/src/components/logs/LogsChart.svelte | 9 +- ui/src/components/settings/ImportPopup.svelte | 93 ++++++++++++++++--- .../settings/PageImportCollections.svelte | 44 ++++----- .../components/users/UserUpsertPanel.svelte | 8 +- 10 files changed, 224 insertions(+), 93 deletions(-) diff --git a/daos/base.go b/daos/base.go index f99c03a6..9c9c1e5a 100644 --- a/daos/base.go +++ b/daos/base.go @@ -183,6 +183,9 @@ func (dao *Dao) create(m models.Model) error { m.RefreshId() } + // mark the model as "new" since the model now always has an ID + m.MarkAsNew() + if m.GetCreated().IsZero() { m.RefreshCreated() } @@ -213,7 +216,7 @@ func (dao *Dao) create(m models.Model) error { } } - // clears the internal isNewFlag + // clears the "new" model flag m.UnmarkAsNew() if dao.AfterCreateFunc != nil { diff --git a/forms/collection_upsert.go b/forms/collection_upsert.go index 1bd6712a..c753c897 100644 --- a/forms/collection_upsert.go +++ b/forms/collection_upsert.go @@ -6,6 +6,7 @@ import ( validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models/schema" "github.com/pocketbase/pocketbase/resolvers" @@ -14,12 +15,12 @@ import ( var collectionNameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_]*$`) -// CollectionUpsert defines a collection upsert (create/update) form. +// CollectionUpsert specifies a [models.Collection] upsert (create/update) form. type CollectionUpsert struct { - app core.App + config CollectionUpsertConfig collection *models.Collection - isCreate bool + Id string `form:"id" json:"id"` Name string `form:"name" json:"name"` System bool `form:"system" json:"system"` Schema schema.Schema `form:"schema" json:"schema"` @@ -30,25 +31,48 @@ type CollectionUpsert struct { DeleteRule *string `form:"deleteRule" json:"deleteRule"` } -// NewCollectionUpsert creates new collection upsert form for the provided Collection model -// (pass an empty Collection model instance (`&models.Collection{}`) for create). +// CollectionUpsertConfig is the [CollectionUpsert] factory initializer config. +// +// NB! Dao is a required struct member. +type CollectionUpsertConfig struct { + Dao *daos.Dao +} + +// NewCollectionUpsert creates a new [CollectionUpsert] form with initializer +// config created from the provided [core.App] and [models.Collection] instances. +// +// This factory method is used primarily for convenience (and backward compatibility). +// If you want to submit the form as part of another transaction, use +// [NewCollectionUpsertWithConfig] with Dao configured to your txDao. func NewCollectionUpsert(app core.App, collection *models.Collection) *CollectionUpsert { - form := &CollectionUpsert{ - app: app, - collection: collection, - isCreate: collection.IsNew(), + form := NewCollectionUpsertWithConfig(CollectionUpsertConfig{ + Dao: app.Dao(), + }, collection) + + return form +} + +// NewCollectionUpsertWithConfig creates a new [CollectionUpsert] form +// with the provided config and [models.Collection] instance or panics on invalid configuration +// (for create you could pass a pointer to an empty Collection instance - `&models.Collection{}`). +func NewCollectionUpsertWithConfig(config CollectionUpsertConfig, collection *models.Collection) *CollectionUpsert { + form := &CollectionUpsert{config: config} + + if form.config.Dao == nil || collection == nil { + panic("Invalid initializer config or nil upsert model.") } // load defaults - form.Name = collection.Name - form.System = collection.System - form.ListRule = collection.ListRule - form.ViewRule = collection.ViewRule - form.CreateRule = collection.CreateRule - form.UpdateRule = collection.UpdateRule - form.DeleteRule = collection.DeleteRule + form.Id = form.collection.Id + form.Name = form.collection.Name + form.System = form.collection.System + form.ListRule = form.collection.ListRule + form.ViewRule = form.collection.ViewRule + form.CreateRule = form.collection.CreateRule + form.UpdateRule = form.collection.UpdateRule + form.DeleteRule = form.collection.DeleteRule - clone, _ := collection.Schema.Clone() + clone, _ := form.collection.Schema.Clone() if clone != nil { form.Schema = *clone } else { @@ -61,6 +85,10 @@ func NewCollectionUpsert(app core.App, collection *models.Collection) *Collectio // Validate makes the form validatable by implementing [validation.Validatable] interface. func (form *CollectionUpsert) Validate() error { return validation.ValidateStruct(form, + validation.Field( + &form.Id, + validation.Length(models.DefaultIdLength, models.DefaultIdLength), + ), validation.Field( &form.System, validation.By(form.ensureNoSystemFlagChange), @@ -90,11 +118,11 @@ func (form *CollectionUpsert) Validate() error { func (form *CollectionUpsert) checkUniqueName(value any) error { v, _ := value.(string) - if !form.app.Dao().IsCollectionNameUnique(v, form.collection.Id) { + if !form.config.Dao.IsCollectionNameUnique(v, form.collection.Id) { return validation.NewError("validation_collection_name_exists", "Collection name must be unique (case insensitive).") } - if (form.isCreate || !strings.EqualFold(v, form.collection.Name)) && form.app.Dao().HasTable(v) { + if (form.collection.IsNew() || !strings.EqualFold(v, form.collection.Name)) && form.config.Dao.HasTable(v) { return validation.NewError("validation_collection_name_table_exists", "The collection name must be also unique table name.") } @@ -104,7 +132,7 @@ func (form *CollectionUpsert) checkUniqueName(value any) error { func (form *CollectionUpsert) ensureNoSystemNameChange(value any) error { v, _ := value.(string) - if form.isCreate || !form.collection.System || v == form.collection.Name { + if form.collection.IsNew() || !form.collection.System || v == form.collection.Name { return nil } @@ -114,7 +142,7 @@ func (form *CollectionUpsert) ensureNoSystemNameChange(value any) error { func (form *CollectionUpsert) ensureNoSystemFlagChange(value any) error { v, _ := value.(bool) - if form.isCreate || v == form.collection.System { + if form.collection.IsNew() || v == form.collection.System { return nil } @@ -161,7 +189,7 @@ func (form *CollectionUpsert) checkRule(value any) error { } dummy := &models.Collection{Schema: form.Schema} - r := resolvers.NewRecordFieldResolver(form.app.Dao(), dummy, nil) + r := resolvers.NewRecordFieldResolver(form.config.Dao, dummy, nil) _, err := search.FilterData(*v).BuildExpr(r) if err != nil { @@ -182,13 +210,19 @@ func (form *CollectionUpsert) Submit(interceptors ...InterceptorFunc) error { return err } - // system flag can be set only for create - if form.isCreate { + if form.collection.IsNew() { + // system flag can be set only on create form.collection.System = form.System + + // custom insertion id can be set only on create + if form.Id != "" { + form.collection.MarkAsNew() + form.collection.SetId(form.Id) + } } // system collections cannot be renamed - if form.isCreate || !form.collection.System { + if form.collection.IsNew() || !form.collection.System { form.collection.Name = form.Name } @@ -200,6 +234,6 @@ func (form *CollectionUpsert) Submit(interceptors ...InterceptorFunc) error { form.collection.DeleteRule = form.DeleteRule return runInterceptors(func() error { - return form.app.Dao().SaveCollection(form.collection) + return form.config.Dao.SaveCollection(form.collection) }, interceptors...) } diff --git a/forms/collections_import.go b/forms/collections_import.go index 10cca676..4d19e03f 100644 --- a/forms/collections_import.go +++ b/forms/collections_import.go @@ -11,24 +11,48 @@ import ( "github.com/pocketbase/pocketbase/models" ) -// CollectionsImport defines a bulk collections import form. +// CollectionsImport specifies a form model to bulk import +// (create, replace and delete) collections from a user provided list. type CollectionsImport struct { - app core.App + config CollectionsImportConfig Collections []*models.Collection `form:"collections" json:"collections"` DeleteOthers bool `form:"deleteOthers" json:"deleteOthers"` } -// NewCollectionsImport bulk imports (create, replace and delete) -// a user provided list with collections data. -func NewCollectionsImport(app core.App) *CollectionsImport { - form := &CollectionsImport{ - app: app, +// CollectionsImportConfig is the [CollectionsImport] factory initializer config. +// +// NB! Dao is a required struct member. +type CollectionsImportConfig struct { + Dao *daos.Dao + IsDebug bool +} + +// NewCollectionsImportWithConfig creates a new [CollectionsImport] +// form with the provided config or panics on invalid configuration. +func NewCollectionsImportWithConfig(config CollectionsImportConfig) *CollectionsImport { + form := &CollectionsImport{config: config} + + if form.config.Dao == nil { + panic("Invalid initializer config.") } return form } +// NewCollectionsImport creates a new [CollectionsImport] form with +// initializer config created from the provided [core.App] instance. +// +// This factory method is used primarily for convenience (and backward compatibility). +// If you want to submit the form as part of another transaction, use +// [NewCollectionsImportWithConfig] with Dao configured to your txDao. +func NewCollectionsImport(app core.App) *CollectionsImport { + return NewCollectionsImportWithConfig(CollectionsImportConfig{ + Dao: app.Dao(), + IsDebug: app.IsDebug(), + }) +} + // Validate makes the form validatable by implementing [validation.Validatable] interface. func (form *CollectionsImport) Validate() error { return validation.ValidateStruct(form, @@ -49,12 +73,12 @@ func (form *CollectionsImport) Submit() error { return err } - // @todo validate id length in the form - return form.app.Dao().RunInTransaction(func(txDao *daos.Dao) error { + return form.config.Dao.RunInTransaction(func(txDao *daos.Dao) error { oldCollections := []*models.Collection{} if err := txDao.CollectionQuery().All(&oldCollections); err != nil { return err } + mappedOldCollections := make(map[string]*models.Collection, len(oldCollections)) for _, old := range oldCollections { mappedOldCollections[old.GetId()] = old @@ -71,7 +95,7 @@ func (form *CollectionsImport) Submit() error { if mappedFormCollections[old.GetId()] == nil { // delete the collection if err := txDao.DeleteCollection(old); err != nil { - if form.app.IsDebug() { + if form.config.IsDebug { log.Println("[CollectionsImport] DeleteOthers failure", old.Name, err) } return validation.Errors{"collections": validation.NewError( @@ -79,6 +103,8 @@ func (form *CollectionsImport) Submit() error { fmt.Sprintf("Failed to delete collection %q (%s). Make sure that the collection is not system or referenced by other collections.", old.Name, old.Id), )} } + + delete(mappedOldCollections, old.GetId()) } } } @@ -91,7 +117,7 @@ func (form *CollectionsImport) Submit() error { } if err := txDao.Save(collection); err != nil { - if form.app.IsDebug() { + if form.config.IsDebug { log.Println("[CollectionsImport] Save failure", collection.Name, err) } return validation.Errors{"collections": validation.NewError( @@ -112,10 +138,13 @@ func (form *CollectionsImport) Submit() error { for _, collection := range refreshedCollections { upsertModel := mappedOldCollections[collection.GetId()] if upsertModel == nil { - upsertModel = &models.Collection{} + upsertModel = collection } - upsertForm := NewCollectionUpsert(form.app, upsertModel) + upsertForm := NewCollectionUpsertWithConfig(CollectionUpsertConfig{ + Dao: txDao, + }, upsertModel) // load form fields with the refreshed collection state + upsertForm.Id = collection.Id upsertForm.Name = collection.Name upsertForm.System = collection.System upsertForm.ListRule = collection.ListRule @@ -125,10 +154,6 @@ func (form *CollectionsImport) Submit() error { upsertForm.DeleteRule = collection.DeleteRule upsertForm.Schema = collection.Schema if err := upsertForm.Validate(); err != nil { - if form.app.IsDebug() { - log.Println("[CollectionsImport] Validate failure", collection.Name, err) - } - // serialize the validation error(s) serializedErr, _ := json.Marshal(err) @@ -143,7 +168,7 @@ func (form *CollectionsImport) Submit() error { for _, collection := range form.Collections { oldCollection := mappedOldCollections[collection.GetId()] if err := txDao.SyncRecordTableSchema(collection, oldCollection); err != nil { - if form.app.IsDebug() { + if form.config.IsDebug { log.Println("[CollectionsImport] Records table sync failure", collection.Name, err) } return validation.Errors{"collections": validation.NewError( diff --git a/models/base.go b/models/base.go index d7725986..b5477836 100644 --- a/models/base.go +++ b/models/base.go @@ -9,6 +9,9 @@ import ( "golang.org/x/crypto/bcrypt" ) +// DefaultIdLength is the default length of the generated model id. +const DefaultIdLength = 15 + // ColumnValueMapper defines an interface for custom db model data serialization. type ColumnValueMapper interface { // ColumnValueMap returns the data to be used when persisting the model. @@ -96,7 +99,7 @@ func (m *BaseModel) GetUpdated() types.DateTime { // // The generated id is a cryptographically random 15 characters length string. func (m *BaseModel) RefreshId() { - m.Id = security.RandomString(15) + m.Id = security.RandomString(DefaultIdLength) } // RefreshCreated updates the model's Created field with the current datetime. diff --git a/ui/package.json b/ui/package.json index def935a6..f064213b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,7 +14,6 @@ "devDependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", - "@codemirror/lang-javascript": "^6.0.2", "@codemirror/language": "^6.0.0", "@codemirror/legacy-modes": "^6.0.0", "@codemirror/search": "^6.0.0", diff --git a/ui/src/components/admins/AdminUpsertPanel.svelte b/ui/src/components/admins/AdminUpsertPanel.svelte index 2b7546ab..4a71cb91 100644 --- a/ui/src/components/admins/AdminUpsertPanel.svelte +++ b/ui/src/components/admins/AdminUpsertPanel.svelte @@ -101,7 +101,8 @@ } confirm(`Do you really want to delete the selected admin?`, () => { - return ApiClient.admins.delete(admin.id) + return ApiClient.admins + .delete(admin.id) .then(() => { confirmClose = false; hide(); @@ -190,7 +191,7 @@ {#if admin.isNew || changePasswordToggle}
-
+