package core import ( "cmp" "context" "database/sql" "encoding/json" "errors" "fmt" "slices" validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/spf13/cast" ) // ImportCollectionsByMarshaledJSON is the same as [ImportCollections] // but accept marshaled json array as import data (usually used for the autogenerated snapshots). func (app *BaseApp) ImportCollectionsByMarshaledJSON(rawSliceOfMaps []byte, deleteMissing bool) error { data := []map[string]any{} err := json.Unmarshal(rawSliceOfMaps, &data) if err != nil { return err } return app.ImportCollections(data, deleteMissing) } // ImportCollections imports the provided collections data in a single transaction. // // For existing matching collections, the imported data is unmarshaled on top of the existing model. // // NB! If deleteMissing is true, ALL NON-SYSTEM COLLECTIONS AND SCHEMA FIELDS, // that are not present in the imported configuration, WILL BE DELETED // (this includes their related records data). func (app *BaseApp) ImportCollections(toImport []map[string]any, deleteMissing bool) error { if len(toImport) == 0 { // prevent accidentally deleting all collections return errors.New("no collections to import") } importedCollections := make([]*Collection, len(toImport)) mappedImported := make(map[string]*Collection, len(toImport)) // normalize imported collections data to ensure that all // collection fields are present and properly initialized for i, data := range toImport { var imported *Collection identifier := cast.ToString(data["id"]) if identifier == "" { identifier = cast.ToString(data["name"]) } existing, err := app.FindCollectionByNameOrId(identifier) if err != nil && !errors.Is(err, sql.ErrNoRows) { return err } if existing != nil { // refetch for deep copy imported, err = app.FindCollectionByNameOrId(existing.Id) if err != nil { return err } // ensure that the fields will be cleared if data["fields"] == nil && deleteMissing { data["fields"] = []map[string]any{} } rawData, err := json.Marshal(data) if err != nil { return err } // load the imported data err = json.Unmarshal(rawData, imported) if err != nil { return err } // extend with the existing fields if necessary for _, f := range existing.Fields { if !f.GetSystem() && deleteMissing { continue } if imported.Fields.GetById(f.GetId()) == nil { // replace with the existing id to prevent accidental column deletion // since otherwise the imported field will be treated as a new one found := imported.Fields.GetByName(f.GetName()) if found != nil && found.Type() == f.Type() { found.SetId(f.GetId()) } imported.Fields.Add(f) } } } else { imported = &Collection{} rawData, err := json.Marshal(data) if err != nil { return err } // load the imported data err = json.Unmarshal(rawData, imported) if err != nil { return err } } imported.IntegrityChecks(false) importedCollections[i] = imported mappedImported[imported.Id] = imported } // reorder views last since the view query could depend on some of the other collections slices.SortStableFunc(importedCollections, func(a, b *Collection) int { cmpA := -1 if a.IsView() { cmpA = 1 } cmpB := -1 if b.IsView() { cmpB = 1 } res := cmp.Compare(cmpA, cmpB) if res == 0 { res = a.Created.Compare(b.Created) if res == 0 { res = a.Updated.Compare(b.Updated) } } return res }) return app.RunInTransaction(func(txApp App) error { existingCollections := []*Collection{} if err := txApp.CollectionQuery().OrderBy("updated ASC").All(&existingCollections); err != nil { return err } mappedExisting := make(map[string]*Collection, len(existingCollections)) for _, existing := range existingCollections { existing.IntegrityChecks(false) mappedExisting[existing.Id] = existing } // delete old collections not available in the new configuration // (before saving the imports in case a deleted collection name is being reused) if deleteMissing { for _, existing := range existingCollections { if mappedImported[existing.Id] != nil || existing.System { continue // exist or system } // delete collection if err := txApp.Delete(existing); err != nil { return err } } } // upsert imported collections for _, imported := range importedCollections { if err := txApp.SaveNoValidate(imported); err != nil { return fmt.Errorf("failed to save collection %q: %w", imported.Name, err) } } // run validations for _, imported := range importedCollections { original := mappedExisting[imported.Id] if original == nil { original = imported } validator := newCollectionValidator( context.Background(), txApp, imported, original, ) if err := validator.run(); err != nil { // serialize the validation error(s) serializedErr, _ := json.MarshalIndent(err, "", " ") return validation.Errors{"collections": validation.NewError( "validation_collections_import_failure", fmt.Sprintf("Data validations failed for collection %q (%s):\n%s", imported.Name, imported.Id, serializedErr), )} } } return nil }) }