From 684f91f7e5942bc5702cb82225f27b7a7dbaf4e8 Mon Sep 17 00:00:00 2001 From: Gani Georgiev Date: Mon, 6 Mar 2023 20:07:34 +0200 Subject: [PATCH] added indirect view update by source collection field change --- daos/collection.go | 66 ++++++++++++++++++++++++++--- daos/collection_test.go | 61 ++++++++++++++++++++++++++ daos/record_table_sync.go | 10 ++--- migrations/1640988000_init.go | 12 +++--- migrations/logs/1640988000_init.go | 4 +- tests/data/logs.db | Bin 1028096 -> 1028096 bytes 6 files changed, 135 insertions(+), 18 deletions(-) diff --git a/daos/collection.go b/daos/collection.go index f565c37f..78ceaf7a 100644 --- a/daos/collection.go +++ b/daos/collection.go @@ -1,6 +1,8 @@ package daos import ( + "bytes" + "encoding/json" "errors" "fmt" "strings" @@ -175,7 +177,9 @@ func (dao *Dao) SaveCollection(collection *models.Collection) error { switch collection.Type { case models.CollectionTypeView: - return txDao.saveViewCollection(collection, oldCollection) + if err := txDao.saveViewCollection(collection, oldCollection); err != nil { + return err + } default: // persist the collection model if err := txDao.Save(collection); err != nil { @@ -183,8 +187,16 @@ func (dao *Dao) SaveCollection(collection *models.Collection) error { } // sync the changes with the related records table - return txDao.SyncRecordTableSchema(collection, oldCollection) + if err := txDao.SyncRecordTableSchema(collection, oldCollection); err != nil { + return err + } } + + // trigger an update for all views with changed schema as a result of the current collection save + // (ignoring view errors to allow users to update the query from the UI) + txDao.resaveViewsWithChangedSchema(collection.Id) + + return nil }) } @@ -329,7 +341,7 @@ func (dao *Dao) ImportCollections( // // This method returns an error if newCollection is not a "view". func (dao *Dao) saveViewCollection(newCollection *models.Collection, oldCollection *models.Collection) error { - if newCollection.IsAuth() { + if !newCollection.IsView() { return errors.New("not a view collection") } @@ -337,7 +349,7 @@ func (dao *Dao) saveViewCollection(newCollection *models.Collection, oldCollecti query := newCollection.ViewOptions().Query // generate collection schema from the query - schema, err := txDao.CreateViewSchema(query) + viewSchema, err := txDao.CreateViewSchema(query) if err != nil { return err } @@ -354,8 +366,52 @@ func (dao *Dao) saveViewCollection(newCollection *models.Collection, oldCollecti return err } - newCollection.Schema = schema + newCollection.Schema = viewSchema return txDao.Save(newCollection) }) } + +// resaveViewsWithChangedSchema updates all view collections with changed schemas. +func (dao *Dao) resaveViewsWithChangedSchema(excludeIds ...string) error { + collections, err := dao.FindCollectionsByType(models.CollectionTypeView) + if err != nil { + return err + } + + return dao.RunInTransaction(func(txDao *Dao) error { + for _, collection := range collections { + if len(excludeIds) > 0 && list.ExistInSlice(collection.Id, excludeIds) { + continue + } + + query := collection.ViewOptions().Query + + // generate a new schema from the query + newSchema, err := txDao.CreateViewSchema(query) + if err != nil { + return err + } + + encodedNewSchema, err := json.Marshal(newSchema) + if err != nil { + return err + } + + encodedOldSchema, err := json.Marshal(collection.Schema) + if err != nil { + return err + } + + if bytes.EqualFold(encodedNewSchema, encodedOldSchema) { + continue // no changes + } + + if err := txDao.saveViewCollection(collection, nil); err != nil { + return err + } + } + + return nil + }) +} diff --git a/daos/collection_test.go b/daos/collection_test.go index 9ac51010..c50c8cff 100644 --- a/daos/collection_test.go +++ b/daos/collection_test.go @@ -11,6 +11,7 @@ import ( "github.com/pocketbase/pocketbase/models/schema" "github.com/pocketbase/pocketbase/tests" "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/types" ) func TestCollectionQuery(t *testing.T) { @@ -305,6 +306,66 @@ func TestSaveCollectionUpdate(t *testing.T) { } } +// indirect update of a field used in view should cause view(s) update +func TestSaveCollectionIndirectViewsUpdate(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, err := app.Dao().FindCollectionByNameOrId("demo1") + if err != nil { + t.Fatal(err) + } + + // update MaxSelect fields + { + relMany := collection.Schema.GetFieldByName("rel_many") + relManyOpt := relMany.Options.(*schema.RelationOptions) + relManyOpt.MaxSelect = types.Pointer(1) + + fileOne := collection.Schema.GetFieldByName("file_one") + fileOneOpt := fileOne.Options.(*schema.FileOptions) + fileOneOpt.MaxSelect = 10 + + if err := app.Dao().SaveCollection(collection); err != nil { + t.Fatal(err) + } + } + + // check view1 schema + { + view1, err := app.Dao().FindCollectionByNameOrId("view1") + if err != nil { + t.Fatal(err) + } + + relMany := view1.Schema.GetFieldByName("rel_many") + relManyOpt := relMany.Options.(*schema.RelationOptions) + if relManyOpt.MaxSelect == nil || *relManyOpt.MaxSelect != 1 { + t.Fatalf("Expected view1.rel_many MaxSelect to be %d, got %v", 1, relManyOpt.MaxSelect) + } + + fileOne := view1.Schema.GetFieldByName("file_one") + fileOneOpt := fileOne.Options.(*schema.FileOptions) + if fileOneOpt.MaxSelect != 10 { + t.Fatalf("Expected view1.file_one MaxSelect to be %d, got %v", 10, fileOneOpt.MaxSelect) + } + } + + // check view2 schema + { + view2, err := app.Dao().FindCollectionByNameOrId("view2") + if err != nil { + t.Fatal(err) + } + + relMany := view2.Schema.GetFieldByName("rel_many") + relManyOpt := relMany.Options.(*schema.RelationOptions) + if relManyOpt.MaxSelect == nil || *relManyOpt.MaxSelect != 1 { + t.Fatalf("Expected view2.rel_many MaxSelect to be %d, got %v", 1, relManyOpt.MaxSelect) + } + } +} + func TestImportCollections(t *testing.T) { totalCollections := 10 diff --git a/daos/record_table_sync.go b/daos/record_table_sync.go index ffd9d9e0..b84ad94f 100644 --- a/daos/record_table_sync.go +++ b/daos/record_table_sync.go @@ -19,9 +19,9 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle // create if oldCollection == nil { cols := map[string]string{ - schema.FieldNameId: "TEXT PRIMARY KEY NOT NULL", - schema.FieldNameCreated: "TEXT DEFAULT '' NOT NULL", - schema.FieldNameUpdated: "TEXT DEFAULT '' NOT NULL", + schema.FieldNameId: "TEXT PRIMARY KEY DEFAULT ('r'||lower(hex(randomblob(7)))) NOT NULL", + schema.FieldNameCreated: "TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL", + schema.FieldNameUpdated: "TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL", } if newCollection.IsAuth() { @@ -154,7 +154,7 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle return err } - return txDao.syncCollectionReferences(newCollection, renamedFieldNames, deletedFieldNames) + return txDao.syncRelationDisplayFieldsChanges(newCollection, renamedFieldNames, deletedFieldNames) }) } @@ -248,7 +248,7 @@ func (dao *Dao) normalizeSingleVsMultipleFieldChanges(newCollection, oldCollecti }) } -func (dao *Dao) syncCollectionReferences(collection *models.Collection, renamedFieldNames map[string]string, deletedFieldNames []string) error { +func (dao *Dao) syncRelationDisplayFieldsChanges(collection *models.Collection, renamedFieldNames map[string]string, deletedFieldNames []string) error { if len(renamedFieldNames) == 0 && len(deletedFieldNames) == 0 { return nil // nothing to sync } diff --git a/migrations/1640988000_init.go b/migrations/1640988000_init.go index eddbddd2..f7121e4b 100644 --- a/migrations/1640988000_init.go +++ b/migrations/1640988000_init.go @@ -42,8 +42,8 @@ func init() { [[tokenKey]] TEXT UNIQUE NOT NULL, [[passwordHash]] TEXT NOT NULL, [[lastResetSentAt]] TEXT DEFAULT "" NOT NULL, - [[created]] TEXT DEFAULT "" NOT NULL, - [[updated]] TEXT DEFAULT "" NOT NULL + [[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL, + [[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL ); CREATE TABLE {{_collections}} ( @@ -58,8 +58,8 @@ func init() { [[updateRule]] TEXT DEFAULT NULL, [[deleteRule]] TEXT DEFAULT NULL, [[options]] JSON DEFAULT "{}" NOT NULL, - [[created]] TEXT DEFAULT "" NOT NULL, - [[updated]] TEXT DEFAULT "" NOT NULL + [[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL, + [[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL ); CREATE TABLE {{_params}} ( @@ -76,8 +76,8 @@ func init() { [[recordId]] TEXT NOT NULL, [[provider]] TEXT NOT NULL, [[providerId]] TEXT NOT NULL, - [[created]] TEXT DEFAULT "" NOT NULL, - [[updated]] TEXT DEFAULT "" NOT NULL, + [[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL, + [[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL, --- FOREIGN KEY ([[collectionId]]) REFERENCES {{_collections}} ([[id]]) ON UPDATE CASCADE ON DELETE CASCADE ); diff --git a/migrations/logs/1640988000_init.go b/migrations/logs/1640988000_init.go index ea2ceb2f..67d10f8a 100644 --- a/migrations/logs/1640988000_init.go +++ b/migrations/logs/1640988000_init.go @@ -20,8 +20,8 @@ func init() { [[referer]] TEXT DEFAULT "" NOT NULL, [[userAgent]] TEXT DEFAULT "" NOT NULL, [[meta]] JSON DEFAULT "{}" NOT NULL, - [[created]] TEXT DEFAULT "" NOT NULL, - [[updated]] TEXT DEFAULT "" NOT NULL + [[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL, + [[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL ); CREATE INDEX _request_status_idx on {{_requests}} ([[status]]); diff --git a/tests/data/logs.db b/tests/data/logs.db index 765c37f3f63fbca7a7d443cbc3018d7070a5a5d5..0dc93ab02837095164fab3f316a6e674c8f929ae 100644 GIT binary patch delta 23279 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 PG2kC#UCjbhe6#-ppLb{e