diff --git a/apis/collection.go b/apis/collection.go index 313bd170..31645c0a 100644 --- a/apis/collection.go +++ b/apis/collection.go @@ -159,7 +159,7 @@ func (api *collectionApi) delete(c echo.Context) error { handlerErr := api.app.OnCollectionBeforeDeleteRequest().Trigger(event, func(e *core.CollectionDeleteEvent) error { if err := api.app.Dao().DeleteCollection(e.Collection); err != nil { - return NewBadRequestError("Failed to delete collection. Make sure that the collection is not referenced by other collections.", err) + return NewBadRequestError("Failed to delete collection due to existing dependency.", err) } return e.HttpContext.NoContent(http.StatusNoContent) diff --git a/daos/collection.go b/daos/collection.go index 3ded0e31..a56ac1b6 100644 --- a/daos/collection.go +++ b/daos/collection.go @@ -150,6 +150,11 @@ func (dao *Dao) DeleteCollection(collection *models.Collection) error { } } + // trigger views resave to check for dependencies + if err := txDao.resaveViewsWithChangedSchema(collection.Id); err != nil { + return fmt.Errorf("The collection has a view dependency - %w", err) + } + return txDao.Delete(collection) }) } @@ -169,7 +174,7 @@ func (dao *Dao) SaveCollection(collection *models.Collection) error { } } - return dao.RunInTransaction(func(txDao *Dao) error { + txErr := dao.RunInTransaction(func(txDao *Dao) error { // set default collection type if collection.Type == "" { collection.Type = models.CollectionTypeBase @@ -192,12 +197,18 @@ func (dao *Dao) SaveCollection(collection *models.Collection) error { } } - // 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 }) + + if txErr != nil { + return txErr + } + + // 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) + dao.resaveViewsWithChangedSchema(collection.Id) + + return nil } // ImportCollections imports the provided collections list within a single transaction. @@ -343,7 +354,7 @@ func (dao *Dao) ImportCollections( // - saves the newCollection // // 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, oldCollection *models.Collection) error { if !newCollection.IsView() { return errors.New("not a view collection") } @@ -358,7 +369,7 @@ func (dao *Dao) saveViewCollection(newCollection *models.Collection, oldCollecti } // delete old renamed view - if oldCollection != nil && newCollection.Name != oldCollection.Name { + if oldCollection != nil { if err := txDao.DeleteView(oldCollection.Name); err != nil { return err } @@ -388,20 +399,32 @@ func (dao *Dao) resaveViewsWithChangedSchema(excludeIds ...string) error { continue } - query := collection.ViewOptions().Query - - // generate a new schema from the query - newSchema, err := txDao.CreateViewSchema(query) + // clone the existing schema so that it is safe for temp modifications + oldSchema, err := collection.Schema.Clone() if err != nil { return err } + // generate a new schema from the query + newSchema, err := txDao.CreateViewSchema(collection.ViewOptions().Query) + if err != nil { + return err + } + + // unset the schema field ids to exclude from the comparison + for _, f := range oldSchema.Fields() { + f.Id = "" + } + for _, f := range newSchema.Fields() { + f.Id = "" + } + encodedNewSchema, err := json.Marshal(newSchema) if err != nil { return err } - encodedOldSchema, err := json.Marshal(collection.Schema) + encodedOldSchema, err := json.Marshal(oldSchema) if err != nil { return err } diff --git a/daos/record_table_sync.go b/daos/record_table_sync.go index b84ad94f..285da94d 100644 --- a/daos/record_table_sync.go +++ b/daos/record_table_sync.go @@ -2,13 +2,17 @@ package daos import ( "fmt" + "strconv" "strings" + validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tools/dbutils" "github.com/pocketbase/pocketbase/tools/list" "github.com/pocketbase/pocketbase/tools/security" + "github.com/spf13/cast" ) // SyncRecordTableSchema compares the two provided collections @@ -16,70 +20,67 @@ import ( // // If `oldCollection` is null, then only `newCollection` is used to create the record table. func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldCollection *models.Collection) error { - // create - if oldCollection == nil { - cols := map[string]string{ - 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", - } + return dao.RunInTransaction(func(txDao *Dao) error { + // create + // ----------------------------------------------------------- + if oldCollection == nil { + cols := map[string]string{ + 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() { - cols[schema.FieldNameUsername] = "TEXT NOT NULL" - cols[schema.FieldNameEmail] = "TEXT DEFAULT '' NOT NULL" - cols[schema.FieldNameEmailVisibility] = "BOOLEAN DEFAULT FALSE NOT NULL" - cols[schema.FieldNameVerified] = "BOOLEAN DEFAULT FALSE NOT NULL" - cols[schema.FieldNameTokenKey] = "TEXT NOT NULL" - cols[schema.FieldNamePasswordHash] = "TEXT NOT NULL" - cols[schema.FieldNameLastResetSentAt] = "TEXT DEFAULT '' NOT NULL" - cols[schema.FieldNameLastVerificationSentAt] = "TEXT DEFAULT '' NOT NULL" - } + if newCollection.IsAuth() { + cols[schema.FieldNameUsername] = "TEXT NOT NULL" + cols[schema.FieldNameEmail] = "TEXT DEFAULT '' NOT NULL" + cols[schema.FieldNameEmailVisibility] = "BOOLEAN DEFAULT FALSE NOT NULL" + cols[schema.FieldNameVerified] = "BOOLEAN DEFAULT FALSE NOT NULL" + cols[schema.FieldNameTokenKey] = "TEXT NOT NULL" + cols[schema.FieldNamePasswordHash] = "TEXT NOT NULL" + cols[schema.FieldNameLastResetSentAt] = "TEXT DEFAULT '' NOT NULL" + cols[schema.FieldNameLastVerificationSentAt] = "TEXT DEFAULT '' NOT NULL" + } - // ensure that the new collection has an id - if !newCollection.HasId() { - newCollection.RefreshId() - newCollection.MarkAsNew() - } + // ensure that the new collection has an id + if !newCollection.HasId() { + newCollection.RefreshId() + newCollection.MarkAsNew() + } - tableName := newCollection.Name + tableName := newCollection.Name - // add schema field definitions - for _, field := range newCollection.Schema.Fields() { - cols[field.Name] = field.ColDefinition() - } + // add schema field definitions + for _, field := range newCollection.Schema.Fields() { + cols[field.Name] = field.ColDefinition() + } - // create table - if _, err := dao.DB().CreateTable(tableName, cols).Execute(); err != nil { - return err - } - - // add named index on the base `created` column - if _, err := dao.DB().CreateIndex(tableName, "_"+newCollection.Id+"_created_idx", "created").Execute(); err != nil { - return err - } - - // add named unique index on the email and tokenKey columns - if newCollection.IsAuth() { - _, err := dao.DB().NewQuery(fmt.Sprintf( - ` - CREATE UNIQUE INDEX _%s_username_idx ON {{%s}} ([[username]]); - CREATE UNIQUE INDEX _%s_email_idx ON {{%s}} ([[email]]) WHERE [[email]] != ''; - CREATE UNIQUE INDEX _%s_tokenKey_idx ON {{%s}} ([[tokenKey]]); - `, - newCollection.Id, tableName, - newCollection.Id, tableName, - newCollection.Id, tableName, - )).Execute() - if err != nil { + // create table + if _, err := txDao.DB().CreateTable(tableName, cols).Execute(); err != nil { return err } + + // add named unique index on the email and tokenKey columns + if newCollection.IsAuth() { + _, err := txDao.DB().NewQuery(fmt.Sprintf( + ` + CREATE UNIQUE INDEX _%s_username_idx ON {{%s}} ([[username]]); + CREATE UNIQUE INDEX _%s_email_idx ON {{%s}} ([[email]]) WHERE [[email]] != ''; + CREATE UNIQUE INDEX _%s_tokenKey_idx ON {{%s}} ([[tokenKey]]); + `, + newCollection.Id, tableName, + newCollection.Id, tableName, + newCollection.Id, tableName, + )).Execute() + if err != nil { + return err + } + } + + return txDao.createCollectionIndexes(newCollection) } - return nil - } - - // update - return dao.RunInTransaction(func(txDao *Dao) error { + // update + // ----------------------------------------------------------- oldTableName := oldCollection.Name newTableName := newCollection.Name oldSchema := oldCollection.Schema @@ -89,12 +90,17 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle // check for renamed table if !strings.EqualFold(oldTableName, newTableName) { - _, err := txDao.DB().RenameTable(oldTableName, newTableName).Execute() + _, err := txDao.DB().RenameTable("{{"+oldTableName+"}}", "{{"+newTableName+"}}").Execute() if err != nil { return err } } + // drop old indexes (if any) + if err := txDao.dropCollectionIndex(oldCollection); err != nil { + return err + } + // check for deleted columns for _, oldField := range oldSchema.Fields() { if f := newSchema.GetFieldById(oldField.Id); f != nil { @@ -103,7 +109,7 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle _, err := txDao.DB().DropColumn(newTableName, oldField.Name).Execute() if err != nil { - return err + return fmt.Errorf("failed to drop column %s - %w", oldField.Name, err) } deletedFieldNames = append(deletedFieldNames, oldField.Name) @@ -126,7 +132,7 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle // add _, err := txDao.DB().AddColumn(newTableName, tempName, field.ColDefinition()).Execute() if err != nil { - return err + return fmt.Errorf("failed to add column %s - %w", field.Name, err) } } else if oldField.Name != field.Name { tempName := field.Name + security.PseudorandomString(5) @@ -135,7 +141,7 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle // rename _, err := txDao.DB().RenameColumn(newTableName, oldField.Name, tempName).Execute() if err != nil { - return err + return fmt.Errorf("failed to rename column %s - %w", oldField.Name, err) } renamedFieldNames[oldField.Name] = field.Name @@ -154,7 +160,11 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle return err } - return txDao.syncRelationDisplayFieldsChanges(newCollection, renamedFieldNames, deletedFieldNames) + if err := txDao.syncRelationDisplayFieldsChanges(newCollection, renamedFieldNames, deletedFieldNames); err != nil { + return err + } + + return txDao.createCollectionIndexes(newCollection) }) } @@ -291,3 +301,66 @@ func (dao *Dao) syncRelationDisplayFieldsChanges(collection *models.Collection, return nil } + +func (dao *Dao) dropCollectionIndex(collection *models.Collection) error { + if collection.IsView() { + return nil // views don't have indexes + } + + return dao.RunInTransaction(func(txDao *Dao) error { + for _, raw := range collection.Indexes { + parsed := dbutils.ParseIndex(cast.ToString(raw)) + + if !parsed.IsValid() { + continue + } + + if _, err := txDao.DB().NewQuery(fmt.Sprintf("DROP INDEX IF EXISTS [[%s]]", parsed.IndexName)).Execute(); err != nil { + return err + } + } + + return nil + }) +} + +func (dao *Dao) createCollectionIndexes(collection *models.Collection) error { + if collection.IsView() { + return nil // views don't have indexes + } + + return dao.RunInTransaction(func(txDao *Dao) error { + // upsert new indexes + // + // note: we are returning validation errors because the indexes cannot be + // validated in a form, aka. before persisting the related collection + // record table changes + errs := validation.Errors{} + for i, idx := range collection.Indexes { + idxString := cast.ToString(idx) + parsed := dbutils.ParseIndex(idxString) + + if !parsed.IsValid() { + errs[strconv.Itoa(i)] = validation.NewError( + "validation_invalid_index_expression", + fmt.Sprintf("Invalid CREATE INDEX expression."), + ) + continue + } + + if _, err := txDao.DB().NewQuery(idxString).Execute(); err != nil { + errs[strconv.Itoa(i)] = validation.NewError( + "validation_invalid_index_expression", + fmt.Sprintf("Failed to create index %s - %v.", parsed.IndexName, err.Error()), + ) + continue + } + } + + if len(errs) > 0 { + return validation.Errors{"indexes": errs} + } + + return nil + }) +} diff --git a/daos/view.go b/daos/view.go index 68d34e91..25b0fc39 100644 --- a/daos/view.go +++ b/daos/view.go @@ -64,6 +64,10 @@ func (dao *Dao) SaveView(name string, selectQuery string) error { // fetch the view table info to ensure that the view was created // because missing tables or columns won't return an error if _, err := txDao.GetTableInfo(name); err != nil { + // manually cleanup previously created view in case the func + // is called in a nested transaction and the error is discarded + txDao.DeleteView(name) + return err } diff --git a/daos/view_test.go b/daos/view_test.go index 1585f1aa..54affd24 100644 --- a/daos/view_test.go +++ b/daos/view_test.go @@ -5,12 +5,33 @@ import ( "fmt" "testing" + "github.com/pocketbase/dbx" + "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/tests" "github.com/pocketbase/pocketbase/tools/list" ) +func ensureNoTempViews(app core.App, t *testing.T) { + var total int + + err := app.Dao().DB().Select("count(*)"). + From("sqlite_schema"). + AndWhere(dbx.HashExp{"type": "view"}). + AndWhere(dbx.NewExp(`[[name]] LIKE '%\_temp\_%' ESCAPE '\'`)). + Limit(1). + Row(&total) + if err != nil { + t.Fatalf("Failed to check for temp views: %v", err) + } + + if total > 0 { + t.Fatalf("Expected all temp views to be deleted, got %d", total) + } +} + func TestDeleteView(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() @@ -34,6 +55,8 @@ func TestDeleteView(t *testing.T) { t.Errorf("[%d - %q] Expected hasErr %v, got %v (%v)", i, s.viewName, s.expectError, hasErr, err) } } + + ensureNoTempViews(app, t) } func TestSaveView(t *testing.T) { @@ -78,7 +101,7 @@ func TestSaveView(t *testing.T) { { "missing table", "123Test", - "select * from missing", + "select id from missing", true, nil, }, @@ -153,6 +176,24 @@ func TestSaveView(t *testing.T) { } } } + + ensureNoTempViews(app, t) +} + +func TestCreateViewSchemaWithDiscardedNestedTransaction(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + app.Dao().RunInTransaction(func(txDao *daos.Dao) error { + _, err := txDao.CreateViewSchema("select id from missing") + if err == nil { + t.Fatal("Expected error, got nil") + } + + return nil + }) + + ensureNoTempViews(app, t) } func TestCreateViewSchema(t *testing.T) { @@ -179,7 +220,7 @@ func TestCreateViewSchema(t *testing.T) { }, { "missing table", - "select * from missing", + "select id from missing", true, nil, }, @@ -403,6 +444,8 @@ func TestCreateViewSchema(t *testing.T) { } } } + + ensureNoTempViews(app, t) } func TestFindRecordByViewFile(t *testing.T) { diff --git a/forms/collection_upsert.go b/forms/collection_upsert.go index cb6172a1..10cc30e4 100644 --- a/forms/collection_upsert.go +++ b/forms/collection_upsert.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "regexp" + "strconv" + "strings" validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/pocketbase/pocketbase/core" @@ -12,6 +14,7 @@ import ( "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models/schema" "github.com/pocketbase/pocketbase/resolvers" + "github.com/pocketbase/pocketbase/tools/dbutils" "github.com/pocketbase/pocketbase/tools/list" "github.com/pocketbase/pocketbase/tools/search" "github.com/pocketbase/pocketbase/tools/types" @@ -30,6 +33,7 @@ type CollectionUpsert struct { Name string `form:"name" json:"name"` System bool `form:"system" json:"system"` Schema schema.Schema `form:"schema" json:"schema"` + Indexes []string `form:"indexes" json:"indexes"` ListRule *string `form:"listRule" json:"listRule"` ViewRule *string `form:"viewRule" json:"viewRule"` CreateRule *string `form:"createRule" json:"createRule"` @@ -56,6 +60,7 @@ func NewCollectionUpsert(app core.App, collection *models.Collection) *Collectio form.Type = form.collection.Type form.Name = form.collection.Name form.System = form.collection.System + form.Indexes = list.ToUniqueStringSlice(form.collection.Indexes) form.ListRule = form.collection.ListRule form.ViewRule = form.collection.ViewRule form.CreateRule = form.collection.CreateRule @@ -154,6 +159,9 @@ func (form *CollectionUpsert) Validate() error { validation.When(isView, validation.Nil), validation.By(form.checkRule), ), + validation.Field(&form.Indexes, + validation.When(isView, validation.Length(0, 0)).Else(validation.By(form.checkIndexes)), + ), validation.Field(&form.Options, validation.By(form.checkOptions)), ) } @@ -380,6 +388,45 @@ func (form *CollectionUpsert) checkRule(value any) error { return nil } +func (form *CollectionUpsert) checkIndexes(value any) error { + v, _ := value.([]string) + + for i, rawIndex := range v { + parsed := dbutils.ParseIndex(rawIndex) + + if !parsed.IsValid() { + return validation.Errors{ + strconv.Itoa(i): validation.NewError( + "validation_invalid_index_expression", + fmt.Sprintf("Invalid CREATE INDEX expression."), + ), + } + } + + if !strings.EqualFold(parsed.TableName, form.Name) { + return validation.Errors{ + strconv.Itoa(i): validation.NewError( + "validation_invalid_index_table", + fmt.Sprintf("The index table must be the same as the collection name."), + ), + } + } + } + + return nil +} + +func (form *CollectionUpsert) dryDao() *daos.Dao { + if form.dao.ConcurrentDB() == form.dao.NonconcurrentDB() { + // it is already in a transaction and therefore use the app concurrent db pool + // to prevent "transaction has already been committed or rolled back" error + return daos.New(form.app.Dao().ConcurrentDB()) + } + + // otherwise use the form noncurrent dao db pool + return daos.New(form.dao.NonconcurrentDB()) +} + func (form *CollectionUpsert) checkOptions(value any) error { v, _ := value.(types.JsonMap) @@ -467,9 +514,13 @@ func (form *CollectionUpsert) Submit(interceptors ...InterceptorFunc[*models.Col form.collection.Name = form.Name } - // view schema is autogenerated on save + // view schema is autogenerated on save and cannot have indexes if !form.collection.IsView() { form.collection.Schema = form.Schema + form.collection.Indexes = append( + types.JsonArray{}, + list.ToInterfaceSlice(list.ToUniqueStringSlice(form.Indexes))..., + ) } form.collection.ListRule = form.ListRule diff --git a/forms/record_upsert.go b/forms/record_upsert.go index c4096732..46b95076 100644 --- a/forms/record_upsert.go +++ b/forms/record_upsert.go @@ -741,10 +741,16 @@ func (form *RecordUpsert) Submit(interceptors ...InterceptorFunc[*models.Record] } // persist the record model - if saveErr := form.dao.SaveRecord(form.record); saveErr != nil { - return fmt.Errorf("failed to save the record: %w", saveErr) + if err := form.dao.SaveRecord(form.record); err != nil { + preparedErr := form.prepareError(err) + if _, ok := preparedErr.(validation.Errors); ok { + return preparedErr + } + return fmt.Errorf("failed to save the record: %w", err) } + // @todo exec before the record save (it is after because of eventual record id change)? + // // upload new files (if any) if err := form.processFilesToUpload(); err != nil { return fmt.Errorf("failed to process the uploaded files: %w", err) @@ -849,3 +855,30 @@ func (form *RecordUpsert) deleteFilesByNamesList(filenames []string) ([]string, return filenames, nil } + +// prepareError parses the provided error and tries to return +// user-friendly validation error(s). +func (form *RecordUpsert) prepareError(err error) error { + msg := strings.ToLower(err.Error()) + + validationErrs := validation.Errors{} + + // check for unique constraint failure + if strings.Contains(msg, "unique constraint failed") { + msg = strings.ReplaceAll(strings.TrimSpace(msg), ",", " ") + + c := form.record.Collection() + for _, f := range c.Schema.Fields() { + // blank space to unify multi-columns lookup + if strings.Contains(msg+" ", fmt.Sprintf("%s.%s ", strings.ToLower(c.Name), f.Name)) { + validationErrs[f.Name] = validation.NewError("validation_not_unique", "Value must be unique") + } + } + } + + if len(validationErrs) > 0 { + return validationErrs + } + + return err +} diff --git a/migrations/1640988000_init.go b/migrations/1640988000_init.go index f7121e4b..b8f0be02 100644 --- a/migrations/1640988000_init.go +++ b/migrations/1640988000_init.go @@ -52,6 +52,7 @@ func init() { [[type]] TEXT DEFAULT "base" NOT NULL, [[name]] TEXT UNIQUE NOT NULL, [[schema]] JSON DEFAULT "[]" NOT NULL, + [[indexes]] JSON DEFAULT "[]" NOT NULL, [[listRule]] TEXT DEFAULT NULL, [[viewRule]] TEXT DEFAULT NULL, [[createRule]] TEXT DEFAULT NULL, diff --git a/migrations/1678470811_add_indexes_column.go b/migrations/1678470811_add_indexes_column.go new file mode 100644 index 00000000..e910d2fe --- /dev/null +++ b/migrations/1678470811_add_indexes_column.go @@ -0,0 +1,34 @@ +package migrations + +import ( + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" +) + +// Adds _collections indexes column (if not already). +func init() { + AppMigrations.Register(func(db dbx.Builder) error { + dao := daos.New(db) + + cols, err := dao.GetTableColumns("_collections") + if err != nil { + return err + } + + for _, col := range cols { + if col == "indexes" { + return nil // already existing (probably via the init migration) + } + } + + _, err = db.AddColumn("_collections", "indexes", `JSON DEFAULT "[]" NOT NULL`).Execute() + + // @todo populate existing indexes... + + return err + }, func(db dbx.Builder) error { + _, err := db.DropColumn("_collections", "indexes").Execute() + + return err + }) +} diff --git a/models/collection.go b/models/collection.go index 24c4f48b..ac81c381 100644 --- a/models/collection.go +++ b/models/collection.go @@ -23,10 +23,11 @@ const ( type Collection struct { BaseModel - Name string `db:"name" json:"name"` - Type string `db:"type" json:"type"` - System bool `db:"system" json:"system"` - Schema schema.Schema `db:"schema" json:"schema"` + Name string `db:"name" json:"name"` + Type string `db:"type" json:"type"` + System bool `db:"system" json:"system"` + Schema schema.Schema `db:"schema" json:"schema"` + Indexes types.JsonArray `db:"indexes" json:"indexes"` // rules ListRule *string `db:"listRule" json:"listRule"` diff --git a/models/collection_test.go b/models/collection_test.go index 7f5738d9..97b784e6 100644 --- a/models/collection_test.go +++ b/models/collection_test.go @@ -75,22 +75,22 @@ func TestCollectionMarshalJSON(t *testing.T) { { "no type", models.Collection{Name: "test"}, - `{"id":"","created":"","updated":"","name":"test","type":"","system":false,"schema":[],"listRule":null,"viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`, + `{"id":"","created":"","updated":"","name":"test","type":"","system":false,"schema":[],"indexes":[],"listRule":null,"viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`, }, { "unknown type + non empty options", - models.Collection{Name: "test", Type: "unknown", ListRule: types.Pointer("test_list"), Options: types.JsonMap{"test": 123}}, - `{"id":"","created":"","updated":"","name":"test","type":"unknown","system":false,"schema":[],"listRule":"test_list","viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`, + models.Collection{Name: "test", Type: "unknown", ListRule: types.Pointer("test_list"), Options: types.JsonMap{"test": 123}, Indexes: types.JsonArray{"idx_test"}}, + `{"id":"","created":"","updated":"","name":"test","type":"unknown","system":false,"schema":[],"indexes":["idx_test"],"listRule":"test_list","viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`, }, { "base type + non empty options", models.Collection{Name: "test", Type: models.CollectionTypeBase, ListRule: types.Pointer("test_list"), Options: types.JsonMap{"test": 123}}, - `{"id":"","created":"","updated":"","name":"test","type":"base","system":false,"schema":[],"listRule":"test_list","viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`, + `{"id":"","created":"","updated":"","name":"test","type":"base","system":false,"schema":[],"indexes":[],"listRule":"test_list","viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`, }, { "auth type + non empty options", models.Collection{BaseModel: models.BaseModel{Id: "test"}, Type: models.CollectionTypeAuth, Options: types.JsonMap{"test": 123, "allowOAuth2Auth": true, "minPasswordLength": 4}}, - `{"id":"test","created":"","updated":"","name":"","type":"auth","system":false,"schema":[],"listRule":null,"viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{"allowEmailAuth":false,"allowOAuth2Auth":true,"allowUsernameAuth":false,"exceptEmailDomains":null,"manageRule":null,"minPasswordLength":4,"onlyEmailDomains":null,"requireEmail":false}}`, + `{"id":"test","created":"","updated":"","name":"","type":"auth","system":false,"schema":[],"indexes":[],"listRule":null,"viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{"allowEmailAuth":false,"allowOAuth2Auth":true,"allowUsernameAuth":false,"exceptEmailDomains":null,"manageRule":null,"minPasswordLength":4,"onlyEmailDomains":null,"requireEmail":false}}`, }, }