Skip to content

Commit

Permalink
refactor: use kong for parsing CLI flags and arguments (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
titusjaka authored Apr 26, 2024
1 parent 41376a5 commit e0307d5
Show file tree
Hide file tree
Showing 17 changed files with 680 additions and 304 deletions.
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
LISTEN=":4040"
DATABASE_DSN=postgresql://postgres:docker@localhost:5432/snippets?sslmode=disable
HTTP_LISTEN=":4040"
POSTGRES_DSN=postgresql://postgres:docker@localhost:5432/snippets?sslmode=disable
API_TOKEN=
28 changes: 28 additions & 0 deletions commands/flags/postgres.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package flags

import (
"database/sql"
"fmt"

_ "github.com/lib/pq" // import pg driver
)

// PostgreSQL represents PostgreSQL connection flags
// and provides methods to open a connection to the database.
type PostgreSQL struct {
DSN string `kong:"required,group='Postgres',name=postgres-dsn,default=localhost,env='POSTGRES_DSN,DATABASE_DSN',help='Data Source Name for PostgreSQL database server.'"`
}

// OpenStdSQLDB opens a new connection to the PostgreSQL database
// using the standard library's sql package.
func (p PostgreSQL) OpenStdSQLDB() (*sql.DB, error) {
db, err := sql.Open("postgres", p.DSN)
if err != nil {
return nil, fmt.Errorf("open PostgreSQL connection: %w", err)
}

if err = db.Ping(); err != nil {
return nil, fmt.Errorf("ping PostgreSQL: %w", err)
}
return db, nil
}
177 changes: 83 additions & 94 deletions commands/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,138 +2,127 @@ package commands

import (
"context"
"database/sql"
"fmt"
"os"
"path"
"path/filepath"
"strconv"
"text/template"
"time"

"github.com/urfave/cli/v2"

"github.com/titusjaka/go-sample/commands/flags"
"github.com/titusjaka/go-sample/internal/infrastructure/log"
"github.com/titusjaka/go-sample/migrate"
"github.com/titusjaka/go-sample/internal/infrastructure/postgres/pgmigrator"
"github.com/titusjaka/go-sample/migrations"
)

const templateContent = `-- +migrate Up
-- +migrate Down
`

// NewMigrateCmd creates a new migrate CLI sub-command
func NewMigrateCmd() *cli.Command {
return &cli.Command{
Name: "migrate",
Usage: "create a new migration, apply (or rollback) migrations to DB.",
Description: "Utility for easy migrations handling.",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "dsn",
Usage: "Data Source Name for PostgreSQL database server",
EnvVars: []string{"DATABASE_DSN", "POSTGRES_DSN"},
Required: true,
},
},
Subcommands: []*cli.Command{
{
Name: "up",
Usage: "apply all migrations to DB",
Action: migrateUp,
},
{
Name: "down",
Usage: "Rollback a [number] of migrations. Pass a [number] as the first argument.",
Action: migrateDown,
},
{
Name: "create",
Usage: "Create a new blank migration file. Pass a [name] as the first argument.",
Action: migrateCreate,
},
},
}
// MigrateCmd implements kong.Command for migrations. To use this command you need to add migrate.Command
// to the application structure and bind a migration source.
//
// Usage:
//
// type App struct {
// // Add other application-related flags here
// // ...
// Migrate commands.MigrateCmd `kong:"cmd,name=migrate,help='Apply database migrations.'"`
// }
//
// func main() {
// var app App
// kCtx := kong.Parse(
// &app,
// // Add other params, if needed
// // …
// )
// kCtx.FatalIfErrorf(kCtx.Run())
// }
//
// CLI usage:
//
// $ go run main.go migrate up
type MigrateCmd struct {
Create CreateCmd `kong:"cmd,name=create,help='Create a new blank migration file. Pass a [name] as the first argument.'"`
Up UpCmd `kong:"cmd,name=up,default=1,help='Apply all database migrations.'"`
Down DownCmd `kong:"cmd,name=down,help='Rollback a [number] of migrations. Pass a [number] as the flag.'"`
}

func migrateUp(c *cli.Context) error {
logger := log.New()
// ============================================================================
// Sub-commands

migrator, err := initMigrator(c.String("dsn"))
if err != nil {
return fmt.Errorf("can't init migrator: %w", err)
}
// CreateCmd represents a CLI sub-command to create a new migration file
type CreateCmd struct {
Directory string `kong:"default='./migrations',help='Directory to store migration files'"`
Name string `kong:"arg,required,help='Migration name'"`
}

applied, err := migrator.Up(context.Background())
if err != nil {
return fmt.Errorf("can't apply migrations: %w", err)
}
// UpCmd represents a CLI sub-command to apply all migrations to DB
type UpCmd struct {
Postgres flags.PostgreSQL `kong:"embed"`
}

logger.Info("👟 ➡ migration(s) applied successfully", log.Field("applied", applied))
return nil
// DownCmd represents a CLI sub-command to rollback a specified number of migrations
type DownCmd struct {
Postgres flags.PostgreSQL `kong:"embed"`
Steps int `kong:"required,default='1',name=steps,help='Number of migrations to revert'"`
}

func migrateDown(c *cli.Context) error {
logger := log.New()
// ============================================================================
// Actions

steps, err := strconv.Atoi(c.Args().First())
if err != nil {
return fmt.Errorf("can't parse number of steps %w", err)
}
// Run (CreateCmd) creates a new migration file
func (c CreateCmd) Run() error {
logger := log.New()

migrator, err := initMigrator(c.String("dsn"))
filename, err := pgmigrator.Create(c.Directory, c.Name)
if err != nil {
return fmt.Errorf("can't init migrator: %w", err)
return err
}

reverted, err := migrator.Down(context.Background(), steps)
if err != nil {
return fmt.Errorf("can't revert migrations: %w", err)
}
logger.Info("🧶 ➡ created new migration", log.Field("path", filename))

logger.Info("🤖 ➡ migration(s) reverted successfully", log.Field("reverted", reverted))
return nil
}

func migrateCreate(c *cli.Context) error {
// Run (UpCmd) applies all migrations to DB
func (c UpCmd) Run() error {
logger := log.New()

dir := filepath.Clean("./migrate/migrations")
err := os.MkdirAll(dir, os.ModePerm)
db, err := c.Postgres.OpenStdSQLDB()
if err != nil {
return fmt.Errorf("can't create directory for migrations: %w", err)
return fmt.Errorf("init DB: %w", err)
}

fileName := fmt.Sprintf("%d_%s.sql", time.Now().Unix(), c.Args().First())
pathName := path.Join(dir, fileName)
file, err := os.Create(filepath.Clean(pathName))
if err != nil {
return fmt.Errorf("can't create migration file (%q): %w", pathName, err)
}
defer func() {
if closeErr := db.Close(); closeErr != nil {
logger.Error("close pg connection", log.Field("err", closeErr))
}
}()

tpl, err := template.New("new_migration").Parse(templateContent)
applied, err := pgmigrator.NewMigrator(db, migrations.Dir).Up(context.Background())
if err != nil {
return fmt.Errorf("can't parse migration template: %w", err)
return fmt.Errorf("apply migrations: %w", err)
}

if err := tpl.Execute(file, nil); err != nil {
return fmt.Errorf("can't execute template: %w", err)
}
logger.Info("👟 ➡ migration(s) applied successfully", log.Field("applied", applied))

logger.Info("🧶 ➡ created new migration", log.Field("path", pathName))
return nil
}

func initMigrator(dsn string) (*migrate.Migrator, error) {
db, err := sql.Open("postgres", dsn)
// Run (DownCmd) reverts a specified number of migrations
func (c DownCmd) Run() error {
logger := log.New()

db, err := c.Postgres.OpenStdSQLDB()
if err != nil {
return nil, fmt.Errorf("can't open db connection: %w", err)
return fmt.Errorf("init DB: %w", err)
}

source, err := migrate.NewEmbeddedSource()
defer func() {
if closeErr := db.Close(); closeErr != nil {
logger.Error("close pg connection", log.Field("err", closeErr))
}
}()

reverted, err := pgmigrator.NewMigrator(db, migrations.Dir).Down(context.Background(), c.Steps)
if err != nil {
return nil, fmt.Errorf("can't create embedded source: %w", err)
return fmt.Errorf("revert migrations: %w", err)
}

return migrate.NewMigrator(db, source), nil
logger.Info("🤖 ➡ migration(s) reverted successfully", log.Field("reverted", reverted))

return nil
}
Loading

0 comments on commit e0307d5

Please sign in to comment.