Adding upstream version 0.28.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
88f1d47ab6
commit
e28c88ef14
933 changed files with 194711 additions and 0 deletions
106
plugins/migratecmd/automigrate.go
Normal file
106
plugins/migratecmd/automigrate.go
Normal file
|
@ -0,0 +1,106 @@
|
|||
package migratecmd
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
// automigrateOnCollectionChange handles the automigration snapshot
|
||||
// generation on collection change request event (create/update/delete).
|
||||
func (p *plugin) automigrateOnCollectionChange(e *core.CollectionRequestEvent) error {
|
||||
var err error
|
||||
var old *core.Collection
|
||||
if !e.Collection.IsNew() {
|
||||
old, err = e.App.FindCollectionByNameOrId(e.Collection.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = e.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
new, err := p.app.FindCollectionByNameOrId(e.Collection.Id)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
|
||||
// for now exclude OAuth2 configs from the migration
|
||||
if old != nil && old.IsAuth() {
|
||||
old.OAuth2.Providers = nil
|
||||
}
|
||||
if new != nil && new.IsAuth() {
|
||||
new.OAuth2.Providers = nil
|
||||
}
|
||||
|
||||
var template string
|
||||
var templateErr error
|
||||
if p.config.TemplateLang == TemplateLangJS {
|
||||
template, templateErr = p.jsDiffTemplate(new, old)
|
||||
} else {
|
||||
template, templateErr = p.goDiffTemplate(new, old)
|
||||
}
|
||||
if templateErr != nil {
|
||||
if errors.Is(templateErr, ErrEmptyTemplate) {
|
||||
return nil // no changes
|
||||
}
|
||||
return fmt.Errorf("failed to resolve template: %w", templateErr)
|
||||
}
|
||||
|
||||
var action string
|
||||
switch {
|
||||
case new == nil:
|
||||
action = "deleted_" + normalizeCollectionName(old.Name)
|
||||
case old == nil:
|
||||
action = "created_" + normalizeCollectionName(new.Name)
|
||||
default:
|
||||
action = "updated_" + normalizeCollectionName(old.Name)
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("%d_%s.%s", time.Now().Unix(), action, p.config.TemplateLang)
|
||||
filePath := filepath.Join(p.config.Dir, name)
|
||||
|
||||
return p.app.RunInTransaction(func(txApp core.App) error {
|
||||
// insert the migration entry
|
||||
_, err := txApp.DB().Insert(core.DefaultMigrationsTable, dbx.Params{
|
||||
"file": name,
|
||||
// use microseconds for more granular applied time in case
|
||||
// multiple collection changes happens at the ~exact time
|
||||
"applied": time.Now().UnixMicro(),
|
||||
}).Execute()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ensure that the local migrations dir exist
|
||||
if err := os.MkdirAll(p.config.Dir, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("failed to create migration dir: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filePath, []byte(template), 0644); err != nil {
|
||||
return fmt.Errorf("failed to save automigrate file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func normalizeCollectionName(name string) string {
|
||||
// adds an extra "_" suffix to the name in case the collection ends
|
||||
// with "test" to prevent accidentally resulting in "_test.go"/"_test.js" files
|
||||
if strings.HasSuffix(strings.ToLower(name), "test") {
|
||||
name += "_"
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
217
plugins/migratecmd/migratecmd.go
Normal file
217
plugins/migratecmd/migratecmd.go
Normal file
|
@ -0,0 +1,217 @@
|
|||
// Package migratecmd adds a new "migrate" command support to a PocketBase instance.
|
||||
//
|
||||
// It also comes with automigrations support and templates generation
|
||||
// (both for JS and GO migration files).
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{
|
||||
// TemplateLang: migratecmd.TemplateLangJS, // default to migratecmd.TemplateLangGo
|
||||
// Automigrate: true,
|
||||
// Dir: "/custom/migrations/dir", // optional template migrations path; default to "pb_migrations" (for JS) and "migrations" (for Go)
|
||||
// })
|
||||
//
|
||||
// Note: To allow running JS migrations you'll need to enable first
|
||||
// [jsvm.MustRegister()].
|
||||
package migratecmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/inflector"
|
||||
"github.com/pocketbase/pocketbase/tools/osutils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Config defines the config options of the migratecmd plugin.
|
||||
type Config struct {
|
||||
// Dir specifies the directory with the user defined migrations.
|
||||
//
|
||||
// If not set it fallbacks to a relative "pb_data/../pb_migrations" (for js)
|
||||
// or "pb_data/../migrations" (for go) directory.
|
||||
Dir string
|
||||
|
||||
// Automigrate specifies whether to enable automigrations.
|
||||
Automigrate bool
|
||||
|
||||
// TemplateLang specifies the template language to use when
|
||||
// generating migrations - js or go (default).
|
||||
TemplateLang string
|
||||
}
|
||||
|
||||
// MustRegister registers the migratecmd plugin to the provided app instance
|
||||
// and panic if it fails.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{})
|
||||
func MustRegister(app core.App, rootCmd *cobra.Command, config Config) {
|
||||
if err := Register(app, rootCmd, config); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Register registers the migratecmd plugin to the provided app instance.
|
||||
func Register(app core.App, rootCmd *cobra.Command, config Config) error {
|
||||
p := &plugin{app: app, config: config}
|
||||
|
||||
if p.config.TemplateLang == "" {
|
||||
p.config.TemplateLang = TemplateLangGo
|
||||
}
|
||||
|
||||
if p.config.Dir == "" {
|
||||
if p.config.TemplateLang == TemplateLangJS {
|
||||
p.config.Dir = filepath.Join(p.app.DataDir(), "../pb_migrations")
|
||||
} else {
|
||||
p.config.Dir = filepath.Join(p.app.DataDir(), "../migrations")
|
||||
}
|
||||
}
|
||||
|
||||
// attach the migrate command
|
||||
if rootCmd != nil {
|
||||
rootCmd.AddCommand(p.createCommand())
|
||||
}
|
||||
|
||||
// watch for collection changes
|
||||
if p.config.Automigrate {
|
||||
p.app.OnCollectionCreateRequest().BindFunc(p.automigrateOnCollectionChange)
|
||||
p.app.OnCollectionUpdateRequest().BindFunc(p.automigrateOnCollectionChange)
|
||||
p.app.OnCollectionDeleteRequest().BindFunc(p.automigrateOnCollectionChange)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type plugin struct {
|
||||
app core.App
|
||||
config Config
|
||||
}
|
||||
|
||||
func (p *plugin) createCommand() *cobra.Command {
|
||||
const cmdDesc = `Supported arguments are:
|
||||
- up - runs all available migrations
|
||||
- down [number] - reverts the last [number] applied migrations
|
||||
- create name - creates new blank migration template file
|
||||
- collections - creates new migration file with snapshot of the local collections configuration
|
||||
- history-sync - ensures that the _migrations history table doesn't have references to deleted migration files
|
||||
`
|
||||
|
||||
command := &cobra.Command{
|
||||
Use: "migrate",
|
||||
Short: "Executes app DB migration scripts",
|
||||
Long: cmdDesc,
|
||||
ValidArgs: []string{"up", "down", "create", "collections"},
|
||||
SilenceUsage: true,
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
cmd := ""
|
||||
if len(args) > 0 {
|
||||
cmd = args[0]
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case "create":
|
||||
if _, err := p.migrateCreateHandler("", args[1:], true); err != nil {
|
||||
return err
|
||||
}
|
||||
case "collections":
|
||||
if _, err := p.migrateCollectionsHandler(args[1:], true); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// note: system migrations are always applied as part of the bootstrap process
|
||||
var list = core.MigrationsList{}
|
||||
list.Copy(core.SystemMigrations)
|
||||
list.Copy(core.AppMigrations)
|
||||
|
||||
runner := core.NewMigrationsRunner(p.app, list)
|
||||
|
||||
if err := runner.Run(args...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func (p *plugin) migrateCreateHandler(template string, args []string, interactive bool) (string, error) {
|
||||
if len(args) < 1 {
|
||||
return "", errors.New("missing migration file name")
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
dir := p.config.Dir
|
||||
|
||||
filename := fmt.Sprintf("%d_%s.%s", time.Now().Unix(), inflector.Snakecase(name), p.config.TemplateLang)
|
||||
|
||||
resultFilePath := path.Join(dir, filename)
|
||||
|
||||
if interactive {
|
||||
confirm := osutils.YesNoPrompt(fmt.Sprintf("Do you really want to create migration %q?", resultFilePath), false)
|
||||
if !confirm {
|
||||
fmt.Println("The command has been cancelled")
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
// get default create template
|
||||
if template == "" {
|
||||
var templateErr error
|
||||
if p.config.TemplateLang == TemplateLangJS {
|
||||
template, templateErr = p.jsBlankTemplate()
|
||||
} else {
|
||||
template, templateErr = p.goBlankTemplate()
|
||||
}
|
||||
if templateErr != nil {
|
||||
return "", fmt.Errorf("failed to resolve create template: %v", templateErr)
|
||||
}
|
||||
}
|
||||
|
||||
// ensure that the migrations dir exist
|
||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// save the migration file
|
||||
if err := os.WriteFile(resultFilePath, []byte(template), 0644); err != nil {
|
||||
return "", fmt.Errorf("failed to save migration file %q: %v", resultFilePath, err)
|
||||
}
|
||||
|
||||
if interactive {
|
||||
fmt.Printf("Successfully created file %q\n", resultFilePath)
|
||||
}
|
||||
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
func (p *plugin) migrateCollectionsHandler(args []string, interactive bool) (string, error) {
|
||||
createArgs := []string{"collections_snapshot"}
|
||||
createArgs = append(createArgs, args...)
|
||||
|
||||
collections := []*core.Collection{}
|
||||
if err := p.app.CollectionQuery().OrderBy("created ASC").All(&collections); err != nil {
|
||||
return "", fmt.Errorf("failed to fetch migrations list: %v", err)
|
||||
}
|
||||
|
||||
var template string
|
||||
var templateErr error
|
||||
if p.config.TemplateLang == TemplateLangJS {
|
||||
template, templateErr = p.jsSnapshotTemplate(collections)
|
||||
} else {
|
||||
template, templateErr = p.goSnapshotTemplate(collections)
|
||||
}
|
||||
if templateErr != nil {
|
||||
return "", fmt.Errorf("failed to resolve template: %v", templateErr)
|
||||
}
|
||||
|
||||
return p.migrateCreateHandler(template, createArgs, interactive)
|
||||
}
|
1337
plugins/migratecmd/migratecmd_test.go
Normal file
1337
plugins/migratecmd/migratecmd_test.go
Normal file
File diff suppressed because it is too large
Load diff
769
plugins/migratecmd/templates.go
Normal file
769
plugins/migratecmd/templates.go
Normal file
|
@ -0,0 +1,769 @@
|
|||
package migratecmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
const (
|
||||
TemplateLangJS = "js"
|
||||
TemplateLangGo = "go"
|
||||
|
||||
// note: this usually should be configurable similar to the jsvm plugin,
|
||||
// but for simplicity is static as users can easily change the
|
||||
// reference path if they use custom dirs structure
|
||||
jsTypesDirective = `/// <reference path="../pb_data/types.d.ts" />` + "\n"
|
||||
)
|
||||
|
||||
var ErrEmptyTemplate = errors.New("empty template")
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// JavaScript templates
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
func (p *plugin) jsBlankTemplate() (string, error) {
|
||||
const template = jsTypesDirective + `migrate((app) => {
|
||||
// add up queries...
|
||||
}, (app) => {
|
||||
// add down queries...
|
||||
})
|
||||
`
|
||||
|
||||
return template, nil
|
||||
}
|
||||
|
||||
func (p *plugin) jsSnapshotTemplate(collections []*core.Collection) (string, error) {
|
||||
// unset timestamp fields
|
||||
var collectionsData = make([]map[string]any, len(collections))
|
||||
for i, c := range collections {
|
||||
data, err := toMap(c)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to serialize %q into a map: %w", c.Name, err)
|
||||
}
|
||||
delete(data, "created")
|
||||
delete(data, "updated")
|
||||
deleteNestedMapKey(data, "oauth2", "providers")
|
||||
collectionsData[i] = data
|
||||
}
|
||||
|
||||
jsonData, err := marhshalWithoutEscape(collectionsData, " ", " ")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to serialize collections list: %w", err)
|
||||
}
|
||||
|
||||
const template = jsTypesDirective + `migrate((app) => {
|
||||
const snapshot = %s;
|
||||
|
||||
return app.importCollections(snapshot, false);
|
||||
}, (app) => {
|
||||
return null;
|
||||
})
|
||||
`
|
||||
|
||||
return fmt.Sprintf(template, string(jsonData)), nil
|
||||
}
|
||||
|
||||
func (p *plugin) jsCreateTemplate(collection *core.Collection) (string, error) {
|
||||
// unset timestamp fields
|
||||
collectionData, err := toMap(collection)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
delete(collectionData, "created")
|
||||
delete(collectionData, "updated")
|
||||
deleteNestedMapKey(collectionData, "oauth2", "providers")
|
||||
|
||||
jsonData, err := marhshalWithoutEscape(collectionData, " ", " ")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to serialize collection: %w", err)
|
||||
}
|
||||
|
||||
const template = jsTypesDirective + `migrate((app) => {
|
||||
const collection = new Collection(%s);
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId(%q);
|
||||
|
||||
return app.delete(collection);
|
||||
})
|
||||
`
|
||||
|
||||
return fmt.Sprintf(template, string(jsonData), collection.Id), nil
|
||||
}
|
||||
|
||||
func (p *plugin) jsDeleteTemplate(collection *core.Collection) (string, error) {
|
||||
// unset timestamp fields
|
||||
collectionData, err := toMap(collection)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
delete(collectionData, "created")
|
||||
delete(collectionData, "updated")
|
||||
deleteNestedMapKey(collectionData, "oauth2", "providers")
|
||||
|
||||
jsonData, err := marhshalWithoutEscape(collectionData, " ", " ")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to serialize collections list: %w", err)
|
||||
}
|
||||
|
||||
const template = jsTypesDirective + `migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId(%q);
|
||||
|
||||
return app.delete(collection);
|
||||
}, (app) => {
|
||||
const collection = new Collection(%s);
|
||||
|
||||
return app.save(collection);
|
||||
})
|
||||
`
|
||||
|
||||
return fmt.Sprintf(template, collection.Id, string(jsonData)), nil
|
||||
}
|
||||
|
||||
func (p *plugin) jsDiffTemplate(new *core.Collection, old *core.Collection) (string, error) {
|
||||
if new == nil && old == nil {
|
||||
return "", errors.New("the diff template require at least one of the collection to be non-nil")
|
||||
}
|
||||
|
||||
if new == nil {
|
||||
return p.jsDeleteTemplate(old)
|
||||
}
|
||||
|
||||
if old == nil {
|
||||
return p.jsCreateTemplate(new)
|
||||
}
|
||||
|
||||
upParts := []string{}
|
||||
downParts := []string{}
|
||||
varName := "collection"
|
||||
|
||||
newMap, err := toMap(new)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
oldMap, err := toMap(old)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// non-fields
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
upDiff := diffMaps(oldMap, newMap, "fields", "created", "updated")
|
||||
if len(upDiff) > 0 {
|
||||
downDiff := diffMaps(newMap, oldMap, "fields", "created", "updated")
|
||||
|
||||
rawUpDiff, err := marhshalWithoutEscape(upDiff, " ", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
rawDownDiff, err := marhshalWithoutEscape(downDiff, " ", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
upParts = append(upParts, "// update collection data")
|
||||
upParts = append(upParts, fmt.Sprintf("unmarshal(%s, %s)", string(rawUpDiff), varName)+"\n")
|
||||
// ---
|
||||
downParts = append(downParts, "// update collection data")
|
||||
downParts = append(downParts, fmt.Sprintf("unmarshal(%s, %s)", string(rawDownDiff), varName)+"\n")
|
||||
}
|
||||
|
||||
// fields
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
oldFieldsSlice, ok := oldMap["fields"].([]any)
|
||||
if !ok {
|
||||
return "", errors.New(`oldMap["fields"] is not []any`)
|
||||
}
|
||||
|
||||
newFieldsSlice, ok := newMap["fields"].([]any)
|
||||
if !ok {
|
||||
return "", errors.New(`newMap["fields"] is not []any`)
|
||||
}
|
||||
|
||||
// deleted fields
|
||||
for i, oldField := range old.Fields {
|
||||
if new.Fields.GetById(oldField.GetId()) != nil {
|
||||
continue // exist
|
||||
}
|
||||
|
||||
rawOldField, err := marhshalWithoutEscape(oldFieldsSlice[i], " ", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
upParts = append(upParts, "// remove field")
|
||||
upParts = append(upParts, fmt.Sprintf("%s.fields.removeById(%q)\n", varName, oldField.GetId()))
|
||||
|
||||
downParts = append(downParts, "// add field")
|
||||
downParts = append(downParts, fmt.Sprintf("%s.fields.addAt(%d, new Field(%s))\n", varName, i, rawOldField))
|
||||
}
|
||||
|
||||
// created fields
|
||||
for i, newField := range new.Fields {
|
||||
if old.Fields.GetById(newField.GetId()) != nil {
|
||||
continue // exist
|
||||
}
|
||||
|
||||
rawNewField, err := marhshalWithoutEscape(newFieldsSlice[i], " ", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
upParts = append(upParts, "// add field")
|
||||
upParts = append(upParts, fmt.Sprintf("%s.fields.addAt(%d, new Field(%s))\n", varName, i, rawNewField))
|
||||
|
||||
downParts = append(downParts, "// remove field")
|
||||
downParts = append(downParts, fmt.Sprintf("%s.fields.removeById(%q)\n", varName, newField.GetId()))
|
||||
}
|
||||
|
||||
// modified fields
|
||||
// (note currently ignoring order-only changes as it comes with too many edge-cases)
|
||||
for i, newField := range new.Fields {
|
||||
var rawNewField, rawOldField []byte
|
||||
|
||||
rawNewField, err = marhshalWithoutEscape(newFieldsSlice[i], " ", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var oldFieldIndex int
|
||||
|
||||
for j, oldField := range old.Fields {
|
||||
if oldField.GetId() == newField.GetId() {
|
||||
rawOldField, err = marhshalWithoutEscape(oldFieldsSlice[j], " ", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
oldFieldIndex = j
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if rawOldField == nil || bytes.Equal(rawNewField, rawOldField) {
|
||||
continue // new field or no change
|
||||
}
|
||||
|
||||
upParts = append(upParts, "// update field")
|
||||
upParts = append(upParts, fmt.Sprintf("%s.fields.addAt(%d, new Field(%s))\n", varName, i, rawNewField))
|
||||
|
||||
downParts = append(downParts, "// update field")
|
||||
downParts = append(downParts, fmt.Sprintf("%s.fields.addAt(%d, new Field(%s))\n", varName, oldFieldIndex, rawOldField))
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
if len(upParts) == 0 && len(downParts) == 0 {
|
||||
return "", ErrEmptyTemplate
|
||||
}
|
||||
|
||||
up := strings.Join(upParts, "\n ")
|
||||
down := strings.Join(downParts, "\n ")
|
||||
|
||||
const template = jsTypesDirective + `migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId(%q)
|
||||
|
||||
%s
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId(%q)
|
||||
|
||||
%s
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
`
|
||||
|
||||
return fmt.Sprintf(
|
||||
template,
|
||||
old.Id, strings.TrimSpace(up),
|
||||
new.Id, strings.TrimSpace(down),
|
||||
), nil
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Go templates
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
func (p *plugin) goBlankTemplate() (string, error) {
|
||||
const template = `package %s
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
// add up queries...
|
||||
|
||||
return nil
|
||||
}, func(app core.App) error {
|
||||
// add down queries...
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
return fmt.Sprintf(template, filepath.Base(p.config.Dir)), nil
|
||||
}
|
||||
|
||||
func (p *plugin) goSnapshotTemplate(collections []*core.Collection) (string, error) {
|
||||
// unset timestamp fields
|
||||
var collectionsData = make([]map[string]any, len(collections))
|
||||
for i, c := range collections {
|
||||
data, err := toMap(c)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to serialize %q into a map: %w", c.Name, err)
|
||||
}
|
||||
delete(data, "created")
|
||||
delete(data, "updated")
|
||||
deleteNestedMapKey(data, "oauth2", "providers")
|
||||
collectionsData[i] = data
|
||||
}
|
||||
|
||||
jsonData, err := marhshalWithoutEscape(collectionsData, "\t\t", "\t")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to serialize collections list: %w", err)
|
||||
}
|
||||
|
||||
const template = `package %s
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
jsonData := ` + "`%s`" + `
|
||||
|
||||
return app.ImportCollectionsByMarshaledJSON([]byte(jsonData), false)
|
||||
}, func(app core.App) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
`
|
||||
return fmt.Sprintf(
|
||||
template,
|
||||
filepath.Base(p.config.Dir),
|
||||
escapeBacktick(string(jsonData)),
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *plugin) goCreateTemplate(collection *core.Collection) (string, error) {
|
||||
// unset timestamp fields
|
||||
collectionData, err := toMap(collection)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
delete(collectionData, "created")
|
||||
delete(collectionData, "updated")
|
||||
deleteNestedMapKey(collectionData, "oauth2", "providers")
|
||||
|
||||
jsonData, err := marhshalWithoutEscape(collectionData, "\t\t", "\t")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to serialize collections list: %w", err)
|
||||
}
|
||||
|
||||
const template = `package %s
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
jsonData := ` + "`%s`" + `
|
||||
|
||||
collection := &core.Collection{}
|
||||
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId(%q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Delete(collection)
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
return fmt.Sprintf(
|
||||
template,
|
||||
filepath.Base(p.config.Dir),
|
||||
escapeBacktick(string(jsonData)),
|
||||
collection.Id,
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *plugin) goDeleteTemplate(collection *core.Collection) (string, error) {
|
||||
// unset timestamp fields
|
||||
collectionData, err := toMap(collection)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
delete(collectionData, "created")
|
||||
delete(collectionData, "updated")
|
||||
deleteNestedMapKey(collectionData, "oauth2", "providers")
|
||||
|
||||
jsonData, err := marhshalWithoutEscape(collectionData, "\t\t", "\t")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to serialize collections list: %w", err)
|
||||
}
|
||||
|
||||
const template = `package %s
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId(%q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Delete(collection)
|
||||
}, func(app core.App) error {
|
||||
jsonData := ` + "`%s`" + `
|
||||
|
||||
collection := &core.Collection{}
|
||||
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
return fmt.Sprintf(
|
||||
template,
|
||||
filepath.Base(p.config.Dir),
|
||||
collection.Id,
|
||||
escapeBacktick(string(jsonData)),
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *plugin) goDiffTemplate(new *core.Collection, old *core.Collection) (string, error) {
|
||||
if new == nil && old == nil {
|
||||
return "", errors.New("the diff template require at least one of the collection to be non-nil")
|
||||
}
|
||||
|
||||
if new == nil {
|
||||
return p.goDeleteTemplate(old)
|
||||
}
|
||||
|
||||
if old == nil {
|
||||
return p.goCreateTemplate(new)
|
||||
}
|
||||
|
||||
upParts := []string{}
|
||||
downParts := []string{}
|
||||
varName := "collection"
|
||||
|
||||
newMap, err := toMap(new)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
oldMap, err := toMap(old)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// non-fields
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
upDiff := diffMaps(oldMap, newMap, "fields", "created", "updated")
|
||||
if len(upDiff) > 0 {
|
||||
downDiff := diffMaps(newMap, oldMap, "fields", "created", "updated")
|
||||
|
||||
rawUpDiff, err := marhshalWithoutEscape(upDiff, "\t\t", "\t")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
rawDownDiff, err := marhshalWithoutEscape(downDiff, "\t\t", "\t")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
upParts = append(upParts, "// update collection data")
|
||||
upParts = append(upParts, goErrIf(fmt.Sprintf("json.Unmarshal([]byte(`%s`), &%s)", escapeBacktick(string(rawUpDiff)), varName)))
|
||||
// ---
|
||||
downParts = append(downParts, "// update collection data")
|
||||
downParts = append(downParts, goErrIf(fmt.Sprintf("json.Unmarshal([]byte(`%s`), &%s)", escapeBacktick(string(rawDownDiff)), varName)))
|
||||
}
|
||||
|
||||
// fields
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
oldFieldsSlice, ok := oldMap["fields"].([]any)
|
||||
if !ok {
|
||||
return "", errors.New(`oldMap["fields"] is not []any`)
|
||||
}
|
||||
|
||||
newFieldsSlice, ok := newMap["fields"].([]any)
|
||||
if !ok {
|
||||
return "", errors.New(`newMap["fields"] is not []any`)
|
||||
}
|
||||
|
||||
// deleted fields
|
||||
for i, oldField := range old.Fields {
|
||||
if new.Fields.GetById(oldField.GetId()) != nil {
|
||||
continue // exist
|
||||
}
|
||||
|
||||
rawOldField, err := marhshalWithoutEscape(oldFieldsSlice[i], "\t\t", "\t")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
upParts = append(upParts, "// remove field")
|
||||
upParts = append(upParts, fmt.Sprintf("%s.Fields.RemoveById(%q)\n", varName, oldField.GetId()))
|
||||
|
||||
downParts = append(downParts, "// add field")
|
||||
downParts = append(downParts, goErrIf(fmt.Sprintf("%s.Fields.AddMarshaledJSONAt(%d, []byte(`%s`))", varName, i, escapeBacktick(string(rawOldField)))))
|
||||
}
|
||||
|
||||
// created fields
|
||||
for i, newField := range new.Fields {
|
||||
if old.Fields.GetById(newField.GetId()) != nil {
|
||||
continue // exist
|
||||
}
|
||||
|
||||
rawNewField, err := marhshalWithoutEscape(newFieldsSlice[i], "\t\t", "\t")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
upParts = append(upParts, "// add field")
|
||||
upParts = append(upParts, goErrIf(fmt.Sprintf("%s.Fields.AddMarshaledJSONAt(%d, []byte(`%s`))", varName, i, escapeBacktick(string(rawNewField)))))
|
||||
|
||||
downParts = append(downParts, "// remove field")
|
||||
downParts = append(downParts, fmt.Sprintf("%s.Fields.RemoveById(%q)\n", varName, newField.GetId()))
|
||||
}
|
||||
|
||||
// modified fields
|
||||
// (note currently ignoring order-only changes as it comes with too many edge-cases)
|
||||
for i, newField := range new.Fields {
|
||||
var rawNewField, rawOldField []byte
|
||||
|
||||
rawNewField, err = marhshalWithoutEscape(newFieldsSlice[i], "\t\t", "\t")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var oldFieldIndex int
|
||||
|
||||
for j, oldField := range old.Fields {
|
||||
if oldField.GetId() == newField.GetId() {
|
||||
rawOldField, err = marhshalWithoutEscape(oldFieldsSlice[j], "\t\t", "\t")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
oldFieldIndex = j
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if rawOldField == nil || bytes.Equal(rawNewField, rawOldField) {
|
||||
continue // new field or no change
|
||||
}
|
||||
|
||||
upParts = append(upParts, "// update field")
|
||||
upParts = append(upParts, goErrIf(fmt.Sprintf("%s.Fields.AddMarshaledJSONAt(%d, []byte(`%s`))", varName, i, escapeBacktick(string(rawNewField)))))
|
||||
|
||||
downParts = append(downParts, "// update field")
|
||||
downParts = append(downParts, goErrIf(fmt.Sprintf("%s.Fields.AddMarshaledJSONAt(%d, []byte(`%s`))", varName, oldFieldIndex, escapeBacktick(string(rawOldField)))))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
if len(upParts) == 0 && len(downParts) == 0 {
|
||||
return "", ErrEmptyTemplate
|
||||
}
|
||||
|
||||
up := strings.Join(upParts, "\n\t\t")
|
||||
down := strings.Join(downParts, "\n\t\t")
|
||||
combined := up + down
|
||||
|
||||
// generate imports
|
||||
// ---
|
||||
var imports string
|
||||
|
||||
if strings.Contains(combined, "json.Unmarshal(") ||
|
||||
strings.Contains(combined, "json.Marshal(") {
|
||||
imports += "\n\t\"encoding/json\"\n"
|
||||
}
|
||||
|
||||
imports += "\n\t\"github.com/pocketbase/pocketbase/core\""
|
||||
imports += "\n\tm \"github.com/pocketbase/pocketbase/migrations\""
|
||||
// ---
|
||||
|
||||
const template = `package %s
|
||||
|
||||
import (%s
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId(%q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
%s
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId(%q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
%s
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
return fmt.Sprintf(
|
||||
template,
|
||||
filepath.Base(p.config.Dir),
|
||||
imports,
|
||||
old.Id, strings.TrimSpace(up),
|
||||
new.Id, strings.TrimSpace(down),
|
||||
), nil
|
||||
}
|
||||
|
||||
func marhshalWithoutEscape(v any, prefix string, indent string) ([]byte, error) {
|
||||
raw, err := json.MarshalIndent(v, prefix, indent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// unescape escaped unicode characters
|
||||
unescaped, err := strconv.Unquote(strings.ReplaceAll(strconv.Quote(string(raw)), `\\u`, `\u`))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(unescaped), nil
|
||||
}
|
||||
|
||||
func escapeBacktick(v string) string {
|
||||
return strings.ReplaceAll(v, "`", "` + \"`\" + `")
|
||||
}
|
||||
|
||||
func goErrIf(v string) string {
|
||||
return "if err := " + v + "; err != nil {\n\t\t\treturn err\n\t\t}\n"
|
||||
}
|
||||
|
||||
func toMap(v any) (map[string]any, error) {
|
||||
raw, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := map[string]any{}
|
||||
|
||||
err = json.Unmarshal(raw, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func diffMaps(old, new map[string]any, excludeKeys ...string) map[string]any {
|
||||
diff := map[string]any{}
|
||||
|
||||
for k, vNew := range new {
|
||||
if slices.Contains(excludeKeys, k) {
|
||||
continue
|
||||
}
|
||||
|
||||
vOld, ok := old[k]
|
||||
if !ok {
|
||||
// new field
|
||||
diff[k] = vNew
|
||||
continue
|
||||
}
|
||||
|
||||
// compare the serialized version of the values in case of slice or other custom type
|
||||
rawOld, _ := json.Marshal(vOld)
|
||||
rawNew, _ := json.Marshal(vNew)
|
||||
|
||||
if !bytes.Equal(rawOld, rawNew) {
|
||||
// if both are maps add recursively only the changed fields
|
||||
vOldMap, ok1 := vOld.(map[string]any)
|
||||
vNewMap, ok2 := vNew.(map[string]any)
|
||||
if ok1 && ok2 {
|
||||
subDiff := diffMaps(vOldMap, vNewMap)
|
||||
if len(subDiff) > 0 {
|
||||
diff[k] = subDiff
|
||||
}
|
||||
} else {
|
||||
diff[k] = vNew
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unset missing fields
|
||||
for k := range old {
|
||||
if _, ok := diff[k]; ok || slices.Contains(excludeKeys, k) {
|
||||
continue // already added
|
||||
}
|
||||
|
||||
if _, ok := new[k]; !ok {
|
||||
diff[k] = nil
|
||||
}
|
||||
}
|
||||
|
||||
return diff
|
||||
}
|
||||
|
||||
func deleteNestedMapKey(data map[string]any, parts ...string) {
|
||||
if len(parts) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(parts) == 1 {
|
||||
delete(data, parts[0])
|
||||
return
|
||||
}
|
||||
|
||||
v, ok := data[parts[0]].(map[string]any)
|
||||
if ok {
|
||||
deleteNestedMapKey(v, parts[1:]...)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue