mirror of
https://github.com/pocketbase/pocketbase.git
synced 2025-01-10 00:43:36 +02:00
241 lines
6.5 KiB
Go
241 lines
6.5 KiB
Go
// 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.Options{
|
|
// TemplateLang: migratecmd.TemplateLangJS, // default to migratecmd.TemplateLangGo
|
|
// Automigrate: true,
|
|
// Dir: "migrations_dir_path", // 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.MustRegisterMigrations].
|
|
package migratecmd
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/AlecAivazis/survey/v2"
|
|
"github.com/pocketbase/pocketbase/core"
|
|
"github.com/pocketbase/pocketbase/migrations"
|
|
"github.com/pocketbase/pocketbase/models"
|
|
"github.com/pocketbase/pocketbase/tools/inflector"
|
|
"github.com/pocketbase/pocketbase/tools/migrate"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// Options defines optional struct to customize the default plugin behavior.
|
|
type Options 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
|
|
}
|
|
|
|
type plugin struct {
|
|
app core.App
|
|
options *Options
|
|
}
|
|
|
|
func MustRegister(app core.App, rootCmd *cobra.Command, options *Options) {
|
|
if err := Register(app, rootCmd, options); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func Register(app core.App, rootCmd *cobra.Command, options *Options) error {
|
|
p := &plugin{app: app}
|
|
|
|
if options != nil {
|
|
p.options = options
|
|
} else {
|
|
p.options = &Options{}
|
|
}
|
|
|
|
if p.options.TemplateLang == "" {
|
|
p.options.TemplateLang = TemplateLangGo
|
|
}
|
|
|
|
if p.options.Dir == "" {
|
|
if p.options.TemplateLang == TemplateLangJS {
|
|
p.options.Dir = filepath.Join(p.app.DataDir(), "../pb_migrations")
|
|
} else {
|
|
p.options.Dir = filepath.Join(p.app.DataDir(), "../migrations")
|
|
}
|
|
}
|
|
|
|
// attach the migrate command
|
|
if rootCmd != nil {
|
|
rootCmd.AddCommand(p.createCommand())
|
|
}
|
|
|
|
// watch for collection changes
|
|
if p.options.Automigrate {
|
|
// refresh the cache right after app bootstap
|
|
p.app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error {
|
|
p.refreshCachedCollections()
|
|
return nil
|
|
})
|
|
|
|
// refresh the cache to ensure that it constains the latest changes
|
|
// when migrations are applied on server start
|
|
p.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
|
p.refreshCachedCollections()
|
|
|
|
cachedCollections, _ := p.getCachedCollections()
|
|
// create a full initial snapshot, if there are no custom
|
|
// migrations but there is already at least 1 collection created,
|
|
// to ensure that the automigrate will work with up-to-date collections data
|
|
if !p.hasCustomMigrations() && len(cachedCollections) > 1 {
|
|
p.migrateCollectionsHandler(nil, false)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
p.app.OnModelAfterCreate().Add(p.afterCollectionChange())
|
|
p.app.OnModelAfterUpdate().Add(p.afterCollectionChange())
|
|
p.app.OnModelAfterDelete().Add(p.afterCollectionChange())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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
|
|
`
|
|
|
|
command := &cobra.Command{
|
|
Use: "migrate",
|
|
Short: "Executes app DB migration scripts",
|
|
ValidArgs: []string{"up", "down", "create", "collections"},
|
|
Long: cmdDesc,
|
|
Run: func(command *cobra.Command, args []string) {
|
|
cmd := ""
|
|
if len(args) > 0 {
|
|
cmd = args[0]
|
|
}
|
|
|
|
switch cmd {
|
|
case "create":
|
|
if err := p.migrateCreateHandler("", args[1:], true); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
case "collections":
|
|
if err := p.migrateCollectionsHandler(args[1:], true); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
default:
|
|
runner, err := migrate.NewRunner(p.app.DB(), migrations.AppMigrations)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
if err := runner.Run(args...); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
},
|
|
}
|
|
|
|
return command
|
|
}
|
|
|
|
func (p *plugin) migrateCreateHandler(template string, args []string, interactive bool) error {
|
|
if len(args) < 1 {
|
|
return fmt.Errorf("Missing migration file name")
|
|
}
|
|
|
|
name := args[0]
|
|
dir := p.options.Dir
|
|
|
|
resultFilePath := path.Join(
|
|
dir,
|
|
fmt.Sprintf("%d_%s.%s", time.Now().Unix(), inflector.Snakecase(name), p.options.TemplateLang),
|
|
)
|
|
|
|
if interactive {
|
|
confirm := false
|
|
prompt := &survey.Confirm{
|
|
Message: fmt.Sprintf("Do you really want to create migration %q?", resultFilePath),
|
|
}
|
|
survey.AskOne(prompt, &confirm)
|
|
if !confirm {
|
|
fmt.Println("The command has been cancelled")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// get default create template
|
|
if template == "" {
|
|
var templateErr error
|
|
if p.options.TemplateLang == TemplateLangJS {
|
|
template, templateErr = p.jsBlankTemplate()
|
|
} else {
|
|
template, templateErr = p.goBlankTemplate()
|
|
}
|
|
if templateErr != nil {
|
|
return fmt.Errorf("Failed to resolve create template: %v\n", 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\n", resultFilePath, err)
|
|
}
|
|
|
|
if interactive {
|
|
fmt.Printf("Successfully created file %q\n", resultFilePath)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *plugin) migrateCollectionsHandler(args []string, interactive bool) error {
|
|
createArgs := []string{"collections_snapshot"}
|
|
createArgs = append(createArgs, args...)
|
|
|
|
collections := []*models.Collection{}
|
|
if err := p.app.Dao().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.options.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)
|
|
}
|