added indirect view update by source collection field change
This commit is contained in:
parent
bce4094134
commit
684f91f7e5
|
@ -1,6 +1,8 @@
|
||||||
package daos
|
package daos
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -175,7 +177,9 @@ func (dao *Dao) SaveCollection(collection *models.Collection) error {
|
||||||
|
|
||||||
switch collection.Type {
|
switch collection.Type {
|
||||||
case models.CollectionTypeView:
|
case models.CollectionTypeView:
|
||||||
return txDao.saveViewCollection(collection, oldCollection)
|
if err := txDao.saveViewCollection(collection, oldCollection); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
// persist the collection model
|
// persist the collection model
|
||||||
if err := txDao.Save(collection); err != nil {
|
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
|
// 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".
|
// This method returns an error if newCollection is not a "view".
|
||||||
func (dao *Dao) saveViewCollection(newCollection *models.Collection, oldCollection *models.Collection) error {
|
func (dao *Dao) saveViewCollection(newCollection *models.Collection, oldCollection *models.Collection) error {
|
||||||
if newCollection.IsAuth() {
|
if !newCollection.IsView() {
|
||||||
return errors.New("not a view collection")
|
return errors.New("not a view collection")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,7 +349,7 @@ func (dao *Dao) saveViewCollection(newCollection *models.Collection, oldCollecti
|
||||||
query := newCollection.ViewOptions().Query
|
query := newCollection.ViewOptions().Query
|
||||||
|
|
||||||
// generate collection schema from the query
|
// generate collection schema from the query
|
||||||
schema, err := txDao.CreateViewSchema(query)
|
viewSchema, err := txDao.CreateViewSchema(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -354,8 +366,52 @@ func (dao *Dao) saveViewCollection(newCollection *models.Collection, oldCollecti
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
newCollection.Schema = schema
|
newCollection.Schema = viewSchema
|
||||||
|
|
||||||
return txDao.Save(newCollection)
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/pocketbase/pocketbase/models/schema"
|
"github.com/pocketbase/pocketbase/models/schema"
|
||||||
"github.com/pocketbase/pocketbase/tests"
|
"github.com/pocketbase/pocketbase/tests"
|
||||||
"github.com/pocketbase/pocketbase/tools/list"
|
"github.com/pocketbase/pocketbase/tools/list"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCollectionQuery(t *testing.T) {
|
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) {
|
func TestImportCollections(t *testing.T) {
|
||||||
totalCollections := 10
|
totalCollections := 10
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,9 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle
|
||||||
// create
|
// create
|
||||||
if oldCollection == nil {
|
if oldCollection == nil {
|
||||||
cols := map[string]string{
|
cols := map[string]string{
|
||||||
schema.FieldNameId: "TEXT PRIMARY KEY NOT NULL",
|
schema.FieldNameId: "TEXT PRIMARY KEY DEFAULT ('r'||lower(hex(randomblob(7)))) NOT NULL",
|
||||||
schema.FieldNameCreated: "TEXT DEFAULT '' NOT NULL",
|
schema.FieldNameCreated: "TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL",
|
||||||
schema.FieldNameUpdated: "TEXT DEFAULT '' NOT NULL",
|
schema.FieldNameUpdated: "TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL",
|
||||||
}
|
}
|
||||||
|
|
||||||
if newCollection.IsAuth() {
|
if newCollection.IsAuth() {
|
||||||
|
@ -154,7 +154,7 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle
|
||||||
return err
|
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 {
|
if len(renamedFieldNames) == 0 && len(deletedFieldNames) == 0 {
|
||||||
return nil // nothing to sync
|
return nil // nothing to sync
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,8 +42,8 @@ func init() {
|
||||||
[[tokenKey]] TEXT UNIQUE NOT NULL,
|
[[tokenKey]] TEXT UNIQUE NOT NULL,
|
||||||
[[passwordHash]] TEXT NOT NULL,
|
[[passwordHash]] TEXT NOT NULL,
|
||||||
[[lastResetSentAt]] TEXT DEFAULT "" NOT NULL,
|
[[lastResetSentAt]] TEXT DEFAULT "" NOT NULL,
|
||||||
[[created]] TEXT DEFAULT "" NOT NULL,
|
[[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
|
||||||
[[updated]] TEXT DEFAULT "" NOT NULL
|
[[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE {{_collections}} (
|
CREATE TABLE {{_collections}} (
|
||||||
|
@ -58,8 +58,8 @@ func init() {
|
||||||
[[updateRule]] TEXT DEFAULT NULL,
|
[[updateRule]] TEXT DEFAULT NULL,
|
||||||
[[deleteRule]] TEXT DEFAULT NULL,
|
[[deleteRule]] TEXT DEFAULT NULL,
|
||||||
[[options]] JSON DEFAULT "{}" NOT NULL,
|
[[options]] JSON DEFAULT "{}" NOT NULL,
|
||||||
[[created]] TEXT DEFAULT "" NOT NULL,
|
[[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
|
||||||
[[updated]] TEXT DEFAULT "" NOT NULL
|
[[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE {{_params}} (
|
CREATE TABLE {{_params}} (
|
||||||
|
@ -76,8 +76,8 @@ func init() {
|
||||||
[[recordId]] TEXT NOT NULL,
|
[[recordId]] TEXT NOT NULL,
|
||||||
[[provider]] TEXT NOT NULL,
|
[[provider]] TEXT NOT NULL,
|
||||||
[[providerId]] TEXT NOT NULL,
|
[[providerId]] TEXT NOT NULL,
|
||||||
[[created]] TEXT DEFAULT "" NOT NULL,
|
[[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
|
||||||
[[updated]] TEXT DEFAULT "" 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
|
FOREIGN KEY ([[collectionId]]) REFERENCES {{_collections}} ([[id]]) ON UPDATE CASCADE ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,8 +20,8 @@ func init() {
|
||||||
[[referer]] TEXT DEFAULT "" NOT NULL,
|
[[referer]] TEXT DEFAULT "" NOT NULL,
|
||||||
[[userAgent]] TEXT DEFAULT "" NOT NULL,
|
[[userAgent]] TEXT DEFAULT "" NOT NULL,
|
||||||
[[meta]] JSON DEFAULT "{}" NOT NULL,
|
[[meta]] JSON DEFAULT "{}" NOT NULL,
|
||||||
[[created]] TEXT DEFAULT "" NOT NULL,
|
[[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
|
||||||
[[updated]] TEXT DEFAULT "" NOT NULL
|
[[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX _request_status_idx on {{_requests}} ([[status]]);
|
CREATE INDEX _request_status_idx on {{_requests}} ([[status]]);
|
||||||
|
|
Binary file not shown.
Loading…
Reference in New Issue