package lagoon import ( "errors" "fmt" "strings" "unicode" "git.golem15.com/golem15/summercms/lagoon/attach" "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_" var ( // ErrUnknownPlugin is returned when migrate:rollback names a plugin that is not activated. ErrUnknownPlugin = errors.New("lagoon: plugin is not activated") // ErrNoMigrations is returned when there is no plugin migration set to roll back. ErrNoMigrations = errors.New("lagoon: no plugin migrations to roll back") ) // 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 the framework-owned system_files set first, then 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") } m, err := migrator(gdb, "summercms.attach", attach.Migrations) if err != nil { return err } if err := m.Migrate(); err != nil { return fmt.Errorf("lagoon: migrate system_files: %w", err) } 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 ErrNoMigrations } var target party.Plugin for _, p := range plugins { if p.ID() == pluginID { target = p break } } if target == nil { return fmt.Errorf("%w: %q", ErrUnknownPlugin, pluginID) } hm, ok := target.(pact.HasMigrations) if !ok || len(hm.Migrations()) == 0 { return fmt.Errorf("%w: %q", ErrNoMigrations, pluginID) } m, err := migrator(gdb, target.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 } 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 }