github.com/icyphox/x@v0.0.355-0.20220311094250-029bd783e8b8/popx/migration_box.go (about)

     1  package popx
     2  
     3  import (
     4  	"io"
     5  	"io/fs"
     6  	"sort"
     7  	"strings"
     8  
     9  	"github.com/gobuffalo/pop/v6"
    10  	"github.com/pkg/errors"
    11  
    12  	"github.com/ory/x/logrusx"
    13  )
    14  
    15  type (
    16  	// MigrationBox is a embed migration box.
    17  	MigrationBox struct {
    18  		*Migrator
    19  
    20  		Dir              fs.FS
    21  		l                *logrusx.Logger
    22  		migrationContent MigrationContent
    23  		goMigrations     Migrations
    24  	}
    25  	MigrationContent func(mf Migration, c *pop.Connection, r []byte, usingTemplate bool) (string, error)
    26  	GoMigration      func(c *pop.Tx) error
    27  )
    28  
    29  func WithTemplateValues(v map[string]interface{}) func(*MigrationBox) *MigrationBox {
    30  	return func(m *MigrationBox) *MigrationBox {
    31  		m.migrationContent = ParameterizedMigrationContent(v)
    32  		return m
    33  	}
    34  }
    35  
    36  func WithMigrationContentMiddleware(middleware func(content string, err error) (string, error)) func(*MigrationBox) *MigrationBox {
    37  	return func(m *MigrationBox) *MigrationBox {
    38  		prev := m.migrationContent
    39  		m.migrationContent = func(mf Migration, c *pop.Connection, r []byte, usingTemplate bool) (string, error) {
    40  			return middleware(prev(mf, c, r, usingTemplate))
    41  		}
    42  		return m
    43  	}
    44  }
    45  
    46  // WithGoMigrations adds migrations that have a custom migration runner.
    47  // TEST THEM THOROUGHLY!
    48  // It will be very hard to fix a buggy migration.
    49  func WithGoMigrations(migrations Migrations) func(*MigrationBox) *MigrationBox {
    50  	return func(m *MigrationBox) *MigrationBox {
    51  		m.goMigrations = migrations
    52  		return m
    53  	}
    54  }
    55  
    56  // NewMigrationBox creates a new migration box.
    57  func NewMigrationBox(dir fs.FS, m *Migrator, opts ...func(*MigrationBox) *MigrationBox) (*MigrationBox, error) {
    58  	mb := &MigrationBox{
    59  		Migrator:         m,
    60  		Dir:              dir,
    61  		l:                m.l,
    62  		migrationContent: ParameterizedMigrationContent(nil),
    63  	}
    64  
    65  	for _, o := range opts {
    66  		mb = o(mb)
    67  	}
    68  
    69  	runner := func(b []byte) func(Migration, *pop.Connection, *pop.Tx) error {
    70  		return func(mf Migration, c *pop.Connection, tx *pop.Tx) error {
    71  			content, err := mb.migrationContent(mf, c, b, true)
    72  			if err != nil {
    73  				return errors.Wrapf(err, "error processing %s", mf.Path)
    74  			}
    75  			if content == "" {
    76  				m.l.WithField("migration", mf.Path).Trace("This is usually ok - ignoring migration because content is empty. This is ok!")
    77  				return nil
    78  			}
    79  			if _, err = tx.Exec(content); err != nil {
    80  				return errors.Wrapf(err, "error executing %s, sql: %s", mf.Path, content)
    81  			}
    82  			return nil
    83  		}
    84  	}
    85  
    86  	err := mb.findMigrations(runner)
    87  	if err != nil {
    88  		return mb, err
    89  	}
    90  
    91  	for _, migration := range mb.goMigrations {
    92  		mb.Migrations[migration.Direction] = append(mb.Migrations[migration.Direction], migration)
    93  	}
    94  
    95  	return mb, nil
    96  }
    97  
    98  func (fm *MigrationBox) findMigrations(runner func([]byte) func(mf Migration, c *pop.Connection, tx *pop.Tx) error) error {
    99  	return fs.WalkDir(fm.Dir, ".", func(p string, info fs.DirEntry, err error) error {
   100  		if err != nil {
   101  			return errors.WithStack(err)
   102  		}
   103  
   104  		if info.IsDir() {
   105  			return nil
   106  		}
   107  
   108  		match, err := pop.ParseMigrationFilename(info.Name())
   109  		if err != nil {
   110  			if strings.HasPrefix(err.Error(), "unsupported dialect") {
   111  				fm.l.Tracef("This is usually ok - ignoring migration file %s because dialect is not supported: %s", info.Name(), err.Error())
   112  				return nil
   113  			}
   114  			return errors.WithStack(err)
   115  		}
   116  
   117  		if match == nil {
   118  			fm.l.Tracef("This is usually ok - ignoring migration file %s because it does not match the file pattern.", info.Name())
   119  			return nil
   120  		}
   121  
   122  		f, err := fm.Dir.Open(p)
   123  		if err != nil {
   124  			return errors.WithStack(err)
   125  		}
   126  		content, err := io.ReadAll(f)
   127  		if err != nil {
   128  			return errors.WithStack(err)
   129  		}
   130  
   131  		mf := Migration{
   132  			Path:      p,
   133  			Version:   match.Version,
   134  			Name:      match.Name,
   135  			DBType:    match.DBType,
   136  			Direction: match.Direction,
   137  			Type:      match.Type,
   138  			Runner:    runner(content),
   139  		}
   140  		fm.Migrations[mf.Direction] = append(fm.Migrations[mf.Direction], mf)
   141  		mod := sortIdent(fm.Migrations[mf.Direction])
   142  		if mf.Direction == "down" {
   143  			mod = sort.Reverse(mod)
   144  		}
   145  		sort.Sort(mod)
   146  		return nil
   147  	})
   148  }