mirror of
https://github.com/pocketbase/pocketbase.git
synced 2025-01-27 23:46:18 +02:00
195 lines
4.9 KiB
Go
195 lines
4.9 KiB
Go
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 {
|
|
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
|
|
})
|
|
}
|