1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2024-11-21 13:35:49 +02:00

tweaked automigrate to check for git status and extracted the base flags from the plugins

This commit is contained in:
Gani Georgiev 2022-11-26 22:33:27 +02:00
parent 8c9b657132
commit 675d459137
11 changed files with 170 additions and 367 deletions

4
.gitignore vendored
View File

@ -6,10 +6,6 @@
# goreleaser builds folder
/.builds/
# examples app directories
pb_data
pb_public
# tests coverage
coverage.out

View File

@ -1,241 +0,0 @@
package cmd
import (
"encoding/json"
"fmt"
"log"
"os"
"path"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/migrations/logs"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tools/inflector"
"github.com/pocketbase/pocketbase/tools/migrate"
"github.com/spf13/cobra"
)
// NewMigrateCommand creates and returns new command for handling DB migrations.
func NewMigrateCommand(app core.App) *cobra.Command {
desc := `
Supported arguments are:
- up - runs all available migrations.
- down [number] - reverts the last [number] applied migrations.
- create name [folder] - creates new migration template file.
- collections [folder] - (Experimental) creates new migration file with the most recent local collections configuration.
`
var databaseFlag string
command := &cobra.Command{
Use: "migrate",
Short: "Executes DB migration scripts",
ValidArgs: []string{"up", "down", "create", "collections"},
Long: desc,
Run: func(command *cobra.Command, args []string) {
cmd := ""
if len(args) > 0 {
cmd = args[0]
}
// additional commands
// ---
if cmd == "create" {
if err := migrateCreateHandler(defaultMigrateCreateTemplate, args[1:]); err != nil {
log.Fatal(err)
}
return
}
if cmd == "collections" {
if err := migrateCollectionsHandler(app, args[1:]); err != nil {
log.Fatal(err)
}
return
}
// ---
// normalize
if databaseFlag != "logs" {
databaseFlag = "db"
}
connections := migrationsConnectionsMap(app)
runner, err := migrate.NewRunner(
connections[databaseFlag].DB,
connections[databaseFlag].MigrationsList,
)
if err != nil {
log.Fatal(err)
}
if err := runner.Run(args...); err != nil {
log.Fatal(err)
}
},
}
command.PersistentFlags().StringVar(
&databaseFlag,
"database",
"db",
"specify the database connection to use (db or logs)",
)
return command
}
type migrationsConnection struct {
DB *dbx.DB
MigrationsList migrate.MigrationsList
}
func migrationsConnectionsMap(app core.App) map[string]migrationsConnection {
return map[string]migrationsConnection{
"db": {
DB: app.DB(),
MigrationsList: migrations.AppMigrations,
},
"logs": {
DB: app.LogsDB(),
MigrationsList: logs.LogsMigrations,
},
}
}
// -------------------------------------------------------------------
// migrate create
// -------------------------------------------------------------------
const defaultMigrateCreateTemplate = `package migrations
import (
"github.com/pocketbase/dbx"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(db dbx.Builder) error {
// add up queries...
return nil
}, func(db dbx.Builder) error {
// add down queries...
return nil
})
}
`
func migrateCreateHandler(template string, args []string) error {
if len(args) < 1 {
return fmt.Errorf("Missing migration file name")
}
name := args[0]
var dir string
if len(args) == 2 {
dir = args[1]
}
if dir == "" {
// If not specified, auto point to the default migrations folder.
//
// NB!
// Since the create command makes sense only during development,
// it is expected the user to be in the app working directory
// and to be using `go run`
wd, err := os.Getwd()
if err != nil {
return err
}
dir = path.Join(wd, "migrations")
}
resultFilePath := path.Join(
dir,
fmt.Sprintf("%d_%s.go", time.Now().Unix(), inflector.Snakecase(name)),
)
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
}
// ensure that migrations dir exist
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}
if err := os.WriteFile(resultFilePath, []byte(template), 0644); err != nil {
return fmt.Errorf("Failed to save migration file %q\n", resultFilePath)
}
fmt.Printf("Successfully created file %q\n", resultFilePath)
return nil
}
// -------------------------------------------------------------------
// migrate collections
// -------------------------------------------------------------------
const collectionsMigrateCreateTemplate = `package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models"
)
// Auto generated migration with the most recent collections configuration.
func init() {
m.Register(func(db dbx.Builder) error {
jsonData := ` + "`" + `%s` + "`" + `
collections := []*models.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collections); err != nil {
return err
}
return daos.New(db).ImportCollections(collections, true, nil)
}, func(db dbx.Builder) error {
// no revert since the configuration on the environment, on which
// the migration was executed, could have changed via the UI/API
return nil
})
}
`
func migrateCollectionsHandler(app core.App, args []string) error {
createArgs := []string{"collections_snapshot"}
createArgs = append(createArgs, args...)
dao := daos.New(app.DB())
collections := []*models.Collection{}
if err := dao.CollectionQuery().OrderBy("created ASC").All(&collections); err != nil {
return fmt.Errorf("Failed to fetch migrations list: %v", err)
}
serialized, err := json.MarshalIndent(collections, "\t\t", "\t")
if err != nil {
return fmt.Errorf("Failed to serialize collections list: %v", err)
}
return migrateCreateHandler(
fmt.Sprintf(collectionsMigrateCreateTemplate, string(serialized)),
createArgs,
)
}

View File

@ -10,8 +10,11 @@ import (
"github.com/fatih/color"
"github.com/labstack/echo/v5/middleware"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/migrations/logs"
"github.com/pocketbase/pocketbase/tools/migrate"
"github.com/spf13/cobra"
"golang.org/x/crypto/acme"
@ -38,7 +41,7 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command {
// (or if this is the first time the init migration was executed)
if err := app.RefreshSettings(); err != nil {
color.Yellow("=====================================")
color.Yellow("WARNING - Settings load error! \n%v", err)
color.Yellow("WARNING: Settings load error! \n%v", err)
color.Yellow("Fallback to the application defaults.")
color.Yellow("=====================================")
}
@ -137,8 +140,22 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command {
return command
}
type migrationsConnection struct {
DB *dbx.DB
MigrationsList migrate.MigrationsList
}
func runMigrations(app core.App) error {
connections := migrationsConnectionsMap(app)
connections := []migrationsConnection{
{
DB: app.DB(),
MigrationsList: migrations.AppMigrations,
},
{
DB: app.LogsDB(),
MigrationsList: logs.LogsMigrations,
},
}
for _, c := range connections {
runner, err := migrate.NewRunner(c.DB, c.MigrationsList)

3
examples/base/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
pb_data/
pb_public/
pb_migrations/

View File

@ -2,6 +2,8 @@ package main
import (
"log"
"os"
"os/exec"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/plugins/jsvm"
@ -12,19 +14,65 @@ import (
func main() {
app := pocketbase.New()
// ---------------------------------------------------------------
// Optional plugin flags:
// ---------------------------------------------------------------
var migrationsDir string
app.RootCmd.PersistentFlags().StringVar(
&migrationsDir,
"migrationsDir",
"",
"the directory with the user defined migrations",
)
var automigrate bool
_, gitErr := exec.LookPath("git")
app.RootCmd.PersistentFlags().BoolVar(
&automigrate,
"automigrate",
gitErr == nil,
"enable/disable auto migrations",
)
var publicDir string
app.RootCmd.PersistentFlags().StringVar(
&publicDir,
"publicDir",
"",
"the directory to serve static files",
)
var indexFallback bool
app.RootCmd.PersistentFlags().BoolVar(
&indexFallback,
"indexFallback",
true,
"fallback the request to index.html on missing static path (eg. when pretty urls are used with SPA)",
)
app.RootCmd.ParseFlags(os.Args[1:])
// ---------------------------------------------------------------
// Plugins:
// ---------------------------------------------------------------
// load js pb_migrations
jsvm.MustRegisterMigrationsLoader(app, nil)
jsvm.MustRegisterMigrationsLoader(app, &jsvm.MigrationsLoaderOptions{
Dir: migrationsDir,
})
// migrate command (with js templates)
migratecmd.MustRegister(app, app.RootCmd, &migratecmd.Options{
TemplateLang: migratecmd.TemplateLangJS,
AutoMigrate: true,
Automigrate: automigrate,
Dir: migrationsDir,
})
// pb_public dir
publicdir.MustRegister(app, &publicdir.Options{
FlagsCmd: app.RootCmd,
IndexFallback: true,
Dir: publicDir,
IndexFallback: indexFallback,
})
if err := app.Start(); err != nil {

View File

@ -13,8 +13,9 @@ import (
// MigrationsLoaderOptions defines optional struct to customize the default plugin behavior.
type MigrationsLoaderOptions struct {
// Dir is the app migrations directory from where the js files will be loaded
// (default to pb_data/migrations)
// Dir specifies the directory with the JS migrations.
//
// If not set it fallbacks to a relative "pb_data/../pb_migrations" directory.
Dir string
}

View File

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
@ -17,23 +18,10 @@ import (
)
const migrationsTable = "_migrations"
const automigrateSuffix = "_automigrate"
// tidyMigrationsTable cleanups the migrations table by removing all
// entries with deleted migration files.
func (p *plugin) tidyMigrationsTable() error {
names, filesErr := p.getAllMigrationNames()
if filesErr != nil {
return fmt.Errorf("failed to fetch migration files list: %v", filesErr)
}
_, tidyErr := p.app.Dao().DB().Delete(migrationsTable, dbx.NotIn("file", list.ToInterfaceSlice(names)...)).Execute()
if tidyErr != nil {
return fmt.Errorf("failed to delete last automigrates from the db: %v", tidyErr)
}
return nil
}
// onCollectionChange handles the automigration snapshot generation on
// collection change event (create/update/delete).
func (p *plugin) onCollectionChange() func(*core.ModelEvent) error {
return func(e *core.ModelEvent) error {
if e.Model.TableName() != "_collections" {
@ -48,35 +36,11 @@ func (p *plugin) onCollectionChange() func(*core.ModelEvent) error {
return errors.New("missing collections to automigrate")
}
names, err := p.getAllMigrationNames()
oldFiles, err := p.getAllMigrationNames()
if err != nil {
return fmt.Errorf("failed to fetch migration files list: %v", err)
}
// delete last consequitive automigrates
lastAutomigrates := []string{}
for i := len(names) - 1; i >= 0; i-- {
migrationFile := names[i]
if !strings.Contains(migrationFile, "_automigrate.") {
break
}
lastAutomigrates = append(lastAutomigrates, migrationFile)
}
if len(lastAutomigrates) > 0 {
// delete last automigrates from the db
_, err := p.app.Dao().DB().Delete(migrationsTable, dbx.In("file", list.ToInterfaceSlice(lastAutomigrates)...)).Execute()
if err != nil {
return fmt.Errorf("failed to delete last automigrates from the db: %v", err)
}
// delete last automigrates from the filesystem
for _, f := range lastAutomigrates {
if err := os.Remove(filepath.Join(p.options.Dir, f)); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to delete last automigrates from the filesystem: %v", err)
}
}
}
var template string
var templateErr error
if p.options.TemplateLang == TemplateLangJS {
@ -88,10 +52,6 @@ func (p *plugin) onCollectionChange() func(*core.ModelEvent) error {
return fmt.Errorf("failed to resolve template: %v", templateErr)
}
// add a comment to not edit the template
template = ("// Do not edit by hand since this file is autogenerated and may get overwritten.\n" +
"// If you want to do further changes, create a new non '_automigrate' file instead.\n" + template)
appliedTime := time.Now().Unix()
fileDest := filepath.Join(p.options.Dir, fmt.Sprintf("%d_automigrate.%s", appliedTime, p.options.TemplateLang))
@ -100,11 +60,37 @@ func (p *plugin) onCollectionChange() func(*core.ModelEvent) error {
return fmt.Errorf("failed to create migration dir: %v", err)
}
return os.WriteFile(fileDest, []byte(template), 0644)
if err := os.WriteFile(fileDest, []byte(template), 0644); err != nil {
return fmt.Errorf("failed to save automigrate file: %v", err)
}
// remove the old untracked automigrate file
// (only if the last one was automigrate!)
if len(oldFiles) > 0 && strings.HasSuffix(oldFiles[len(oldFiles)-1], automigrateSuffix+"."+p.options.TemplateLang) {
olfName := oldFiles[len(oldFiles)-1]
oldPath := filepath.Join(p.options.Dir, olfName)
isUntracked := exec.Command(p.options.GitPath, "ls-files", "--error-unmatch", oldPath).Run() != nil
if isUntracked {
// delete the old automigrate from the db if it was already applied
_, err := p.app.Dao().DB().Delete(migrationsTable, dbx.HashExp{"file": olfName}).Execute()
if err != nil {
return fmt.Errorf("failed to delete last applied automigrate from the migration db: %v", err)
}
// delete the old automigrate file from the filesystem
if err := os.Remove(oldPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to delete last automigrates from the filesystem: %v", err)
}
}
}
// getAllMigrationNames return both applied and new local migration file names.
return nil
}
}
// getAllMigrationNames return sorted slice with both applied and new
// local migration file names.
func (p *plugin) getAllMigrationNames() ([]string, error) {
names := []string{}

View File

@ -4,11 +4,13 @@ import (
"fmt"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/fatih/color"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models"
@ -17,10 +19,23 @@ import (
"github.com/spf13/cobra"
)
// Options defines optional struct to customize the default plugin behavior.
type Options struct {
Dir string // the directory with user defined migrations
AutoMigrate bool
// 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
// GitPath is the git cmd binary path (default to just "git").
GitPath string
}
type plugin struct {
@ -55,36 +70,35 @@ func Register(app core.App, rootCmd *cobra.Command, options *Options) error {
}
}
if p.options.GitPath == "" {
p.options.GitPath = "git"
}
// attach the migrate command
if rootCmd != nil {
rootCmd.AddCommand(p.createCommand())
}
// watch for collection changes
if p.options.AutoMigrate {
// @todo replace with AfterBootstrap
p.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
if err := p.tidyMigrationsTable(); err != nil && p.app.IsDebug() {
log.Println("Failed to tidy the migrations table.")
}
return nil
})
if p.options.Automigrate {
if _, err := exec.LookPath(p.options.GitPath); err != nil {
color.Yellow("WARNING: Automigrate cannot be enabled because %s is not installed or accessable.", p.options.GitPath)
} else {
p.app.OnModelAfterCreate().Add(p.onCollectionChange())
p.app.OnModelAfterUpdate().Add(p.onCollectionChange())
p.app.OnModelAfterDelete().Add(p.onCollectionChange())
}
}
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 [folder] - creates new blank migration template file.
- collections [folder] - creates new migration file with the latest local collections snapshot (similar to the automigrate but allows editing).
- up - runs all available migrations
- down [number] - reverts the last [number] applied migrations
- create name [folder] - creates new blank migration template file
- collections [folder] - creates new migration file with the latest local collections snapshot (similar to the automigrate but allows editing)
`
command := &cobra.Command{
@ -98,23 +112,16 @@ func (p *plugin) createCommand() *cobra.Command {
cmd = args[0]
}
// additional commands
// ---
if cmd == "create" {
switch cmd {
case "create":
if err := p.migrateCreateHandler("", args[1:]); err != nil {
log.Fatal(err)
}
return
}
if cmd == "collections" {
case "collections":
if err := p.migrateCollectionsHandler(args[1:]); err != nil {
log.Fatal(err)
}
return
}
// ---
default:
runner, err := migrate.NewRunner(p.app.DB(), migrations.AppMigrations)
if err != nil {
log.Fatal(err)
@ -123,6 +130,7 @@ func (p *plugin) createCommand() *cobra.Command {
if err := runner.Run(args...); err != nil {
log.Fatal(err)
}
}
},
}

View File

@ -13,13 +13,11 @@ import (
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/spf13/cobra"
)
type Options struct {
Dir string
IndexFallback bool
FlagsCmd *cobra.Command
}
type plugin struct {
@ -46,24 +44,6 @@ func Register(app core.App, options *Options) error {
options.Dir = defaultPublicDir()
}
if options.FlagsCmd != nil {
// add "--publicDir" option flag
options.FlagsCmd.PersistentFlags().StringVar(
&options.Dir,
"publicDir",
options.Dir,
"the directory to serve static files",
)
// add "--indexFallback" option flag
options.FlagsCmd.PersistentFlags().BoolVar(
&options.IndexFallback,
"indexFallback",
options.IndexFallback,
"fallback the request to index.html on missing static path (eg. when pretty urls are used with SPA)",
)
}
p.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// serves static files from the provided public dir (if exists)
e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS(options.Dir), options.IndexFallback))
@ -79,6 +59,5 @@ func defaultPublicDir() string {
// most likely ran with go run
return "./pb_public"
}
return filepath.Join(os.Args[0], "../pb_public")
}

View File

@ -18,7 +18,13 @@ type TestApp struct {
// EventCalls defines a map to inspect which app events
// (and how many times) were triggered.
//
// The following events are not counted because they execute always:
// - OnBeforeBootstrap
// - OnAfterBootstrap
// - OnBeforeServe
EventCalls map[string]int
TestMailer *TestMailer
}
@ -81,12 +87,6 @@ func NewTestApp(optTestDataDir ...string) (*TestApp, error) {
TestMailer: &TestMailer{},
}
// no need to count since this is executed always
// t.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// t.EventCalls["OnBeforeServe"]++
// return nil
// })
t.OnModelBeforeCreate().Add(func(e *core.ModelEvent) error {
t.EventCalls["OnModelBeforeCreate"]++
return nil

View File

@ -115,9 +115,12 @@ func (r *Runner) Up() ([]string, error) {
continue
}
// ignore empty Up action
if m.Up != nil {
if err := m.Up(tx); err != nil {
return fmt.Errorf("Failed to apply migration %s: %w", m.File, err)
}
}
if err := r.saveAppliedMigration(tx, m.File); err != nil {
return fmt.Errorf("Failed to save applied migration info for %s: %w", m.File, err)
@ -155,9 +158,12 @@ func (r *Runner) Down(toRevertCount int) ([]string, error) {
break
}
// ignore empty Down action
if m.Down != nil {
if err := m.Down(tx); err != nil {
return fmt.Errorf("Failed to revert migration %s: %w", m.File, err)
}
}
if err := r.saveRevertedMigration(tx, m.File); err != nil {
return fmt.Errorf("Failed to save reverted migration info for %s: %w", m.File, err)