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  }