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