1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-01-27 23:46:18 +02:00
pocketbase/core/collection_import.go
2024-09-29 21:09:46 +03:00

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
})
}