package lagoon import ( "fmt" "strings" "unicode" "git.golem15.com/golem15/summercms/pact" "git.golem15.com/golem15/summercms/party" "github.com/go-gormigrate/gormigrate/v2" "gorm.io/gorm" ) const historyTablePrefix = "summer_migrations_" // HistoryTableName returns the isolated gormigrate table for pluginID. func HistoryTableName(pluginID string) (string, error) { if err := validatePluginID(pluginID); err != nil { return "", err } return historyTablePrefix + strings.ReplaceAll(pluginID, ".", "_"), nil } func validatePluginID(id string) error { if id == "" { return fmt.Errorf("lagoon: plugin id is empty") } for _, r := range id { if r == '.' || unicode.IsLower(r) || unicode.IsDigit(r) { continue } return fmt.Errorf("lagoon: plugin id %q is not a valid history table name", id) } if strings.Contains(id, "..") || strings.HasPrefix(id, ".") || strings.HasSuffix(id, ".") { return fmt.Errorf("lagoon: plugin id %q is not a valid history table name", id) } return nil } func migrator(gdb *gorm.DB, pluginID string, migrations []*gormigrate.Migration) (*gormigrate.Gormigrate, error) { table, err := HistoryTableName(pluginID) if err != nil { return nil, err } return gormigrate.New(gdb, &gormigrate.Options{ TableName: table, UseTransaction: true, }, migrations), nil } // Migrate runs each plugin's HasMigrations set in party.Activate order. func Migrate(gdb *gorm.DB, plugins []party.Plugin) error { if gdb == nil { return fmt.Errorf("lagoon: gorm db is nil") } for _, p := range plugins { hm, ok := p.(pact.HasMigrations) if !ok { continue } migrations := hm.Migrations() if len(migrations) == 0 { continue } m, err := migrator(gdb, p.ID(), migrations) if err != nil { return err } if err := m.Migrate(); err != nil { return fmt.Errorf("lagoon: migrate %s: %w", p.ID(), err) } } return nil } // RollbackLast rolls back the last migration of pluginID. func RollbackLast(gdb *gorm.DB, plugins []party.Plugin, pluginID string) error { if gdb == nil { return fmt.Errorf("lagoon: gorm db is nil") } pluginID = strings.TrimSpace(pluginID) if pluginID == "" { pluginID = lastMigrationPlugin(plugins) } if pluginID == "" { return fmt.Errorf("lagoon: no plugin migrations to roll back") } for _, p := range plugins { if p.ID() != pluginID { continue } hm, ok := p.(pact.HasMigrations) if !ok { return fmt.Errorf("lagoon: plugin %q has no migrations", pluginID) } m, err := migrator(gdb, p.ID(), hm.Migrations()) if err != nil { return err } if err := m.RollbackLast(); err != nil { return fmt.Errorf("lagoon: rollback %s: %w", pluginID, err) } return nil } return fmt.Errorf("lagoon: plugin %q is not activated", pluginID) } func lastMigrationPlugin(plugins []party.Plugin) string { for i := len(plugins) - 1; i >= 0; i-- { p := plugins[i] hm, ok := p.(pact.HasMigrations) if !ok || len(hm.Migrations()) == 0 { continue } return p.ID() } return "" } // StatusRow is one plugin's recorded migration ids. type StatusRow struct { Plugin string Table string IDs []string } // Status reads each plugin history table. func Status(gdb *gorm.DB, plugins []party.Plugin) ([]StatusRow, error) { if gdb == nil { return nil, fmt.Errorf("lagoon: gorm db is nil") } var rows []StatusRow for _, p := range plugins { if _, ok := p.(pact.HasMigrations); !ok { continue } table, err := HistoryTableName(p.ID()) if err != nil { return nil, err } row := StatusRow{Plugin: p.ID(), Table: table} if !gdb.Migrator().HasTable(table) { rows = append(rows, row) continue } if err := gdb.Table(table).Order("id").Pluck("id", &row.IDs).Error; err != nil { return nil, fmt.Errorf("lagoon: status %s: %w", p.ID(), err) } if row.IDs == nil { row.IDs = []string{} } rows = append(rows, row) } return rows, nil }