github.com/wawandco/oxplugins@v0.7.11/tools/liquibase/generator.go (about) 1 package liquibase 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io/ioutil" 9 "os" 10 "path/filepath" 11 "strings" 12 "text/template" 13 "time" 14 15 "github.com/gobuffalo/flect" 16 "github.com/spf13/pflag" 17 "github.com/wawandco/oxplugins/plugins" 18 ) 19 20 var ( 21 ErrNameArgMissing = errors.New("name arg missing") 22 ErrInvalidName = errors.New("invalid migration name") 23 ErrInvalidPath = errors.New("invalid path") 24 ) 25 26 var ( 27 // Ensuring we're building a plugin 28 _ plugins.Plugin = (*Generator)(nil) 29 // Ensuring the plugin is a flagparser 30 _ plugins.FlagParser = (*Generator)(nil) 31 ) 32 33 // Generator for liquibase SQL migrations, it generates xml liquibase 34 // for SQL in the root + basedir folder. It uses the argument passed 35 // to determine both the name of the migration and the destination. 36 // Some examples are: 37 // - "ox generate migration name" generates [timestamp]-name.xml 38 // - "ox generate migration folder/name" generates folder/[timestamp]-name.xml 39 // - "ox generate migration name --base migrations" generates migrations/[timestamp]-name.xml 40 type Generator struct { 41 // mockTimestamp is used for testing purposes, it would replace the 42 // timestamp at the beggining of the migration name. 43 mockTimestamp string 44 45 // Basefolder for the migrations, if a path is passed, then we will append that 46 // path to the baseFolder when generating the migration. 47 baseFolder string 48 49 flags *pflag.FlagSet 50 } 51 52 // Name is the name used to identify the generator and also 53 // the plugin 54 func (g Generator) Name() string { 55 return "migration" 56 } 57 58 // Generate a new migration based on the passed args. This needs at least 3 59 // args since the 3rd arg will be used by the generator to build the name of 60 // the migration. 61 func (g Generator) Generate(ctx context.Context, root string, args []string) error { 62 if len(args) < 3 { 63 return ErrNameArgMissing 64 } 65 66 timestamp := time.Now().UTC().Format("20060102150405") 67 if g.mockTimestamp != "" { 68 timestamp = g.mockTimestamp 69 } 70 71 filename, err := g.composeFilename(args[2], timestamp) 72 if err != nil { 73 return err 74 } 75 76 path := g.baseFolder 77 if dir := filepath.Dir(args[2]); dir != "." { 78 path = filepath.Join(g.baseFolder, dir) 79 } 80 81 path = filepath.Join(path, filename) 82 _, err = os.Stat(path) 83 if err == nil { 84 fmt.Printf("[info] %v already exists\n", path) 85 return nil 86 } 87 88 if !os.IsNotExist(err) { 89 return err 90 } 91 92 // Creating the folder 93 err = os.MkdirAll(filepath.Dir(path), 0755) 94 if err != nil { 95 return (err) 96 } 97 98 tmpl, err := template.New("migration-template").Parse(migrationTemplate) 99 if err != nil { 100 return err 101 } 102 103 var tpl bytes.Buffer 104 err = tmpl.Execute(&tpl, strings.ReplaceAll(filename, ".xml", "")) 105 if err != nil { 106 return err 107 } 108 109 err = ioutil.WriteFile(path, tpl.Bytes(), 0655) 110 if err != nil { 111 return err 112 } 113 114 fmt.Printf("[info] migration generated in %v\n", path) 115 return nil 116 } 117 118 // composeFilename from the passed arg and timestamp, if the passed path is 119 // a dot (.) or a folder "/" then it will return ErrInvalidName. 120 func (g Generator) composeFilename(passed, timestamp string) (string, error) { 121 name := filepath.Base(passed) 122 //Should we check the name here ? 123 if name == "." || name == "/" { 124 return "", ErrInvalidName 125 } 126 127 underscoreName := flect.Underscore(name) 128 result := timestamp + "-" + underscoreName + ".xml" 129 130 return result, nil 131 } 132 133 // Parseflags will parse the baseFolder from the --base or -b flag 134 func (g *Generator) ParseFlags(args []string) { 135 g.flags = pflag.NewFlagSet(g.Name(), pflag.ContinueOnError) 136 g.flags.StringVarP(&g.baseFolder, "base", "b", "", "base folder for the migrations") 137 g.flags.Parse(args) //nolint:errcheck,we don't care hence the flag 138 } 139 140 // Flags parsed by the plugin 141 func (g *Generator) Flags() *pflag.FlagSet { 142 return g.flags 143 }