feat: add a new command - db user create (#4)
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
Reviewed-on: #4
This commit is contained in:
parent
d83aaa5beb
commit
5b061dfef3
|
@ -1,173 +1,15 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/cmd/sessions/internal"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/bundb/migrations"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func New() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "db",
|
||||
Usage: "Manages database migrations",
|
||||
Name: "db",
|
||||
Subcommands: []*cli.Command{
|
||||
newMigrateCmd(),
|
||||
newRollbackCmd(),
|
||||
newCreateCmd(),
|
||||
newStatusCmd(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newMigrateCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "migrate",
|
||||
Usage: "Migrates database",
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := zap.L()
|
||||
|
||||
db, err := internal.NewBunDB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("internal.NewBunDB: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = db.Close()
|
||||
}()
|
||||
|
||||
migrator := migrations.NewMigrator(db)
|
||||
|
||||
if err = migrator.Init(c.Context); err != nil {
|
||||
return fmt.Errorf("migrator.Init: %w", err)
|
||||
}
|
||||
|
||||
group, err := migrator.Migrate(c.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrator.Migrate: %w", err)
|
||||
}
|
||||
if group.ID == 0 {
|
||||
logger.Info("there are no new migrations to run")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Info("migrated to "+strconv.FormatInt(group.ID, 10), zap.Int64("id", group.ID))
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newRollbackCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "rollback",
|
||||
Usage: "Rollbacks the last migration group",
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := zap.L()
|
||||
|
||||
db, err := internal.NewBunDB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("internal.NewBunDB: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = db.Close()
|
||||
}()
|
||||
|
||||
migrator := migrations.NewMigrator(db)
|
||||
|
||||
group, err := migrator.Rollback(c.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrator.Rollback: %w", err)
|
||||
}
|
||||
if group.ID == 0 {
|
||||
logger.Info("there are no groups to roll back")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Info("rolled back "+strconv.FormatInt(group.ID, 10), zap.Int64("id", group.ID))
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newCreateCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "create",
|
||||
Usage: "Creates migration",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "go",
|
||||
Usage: "Creates Go migration",
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := zap.L()
|
||||
|
||||
migrator := migrations.NewMigrator(nil)
|
||||
|
||||
mf, err := migrator.CreateGoMigration(c.Context, strings.Join(c.Args().Slice(), "_"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrator.CreateGoMigration: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("created migration", zap.String("name", mf.Name), zap.String("path", mf.Path))
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "sql",
|
||||
Usage: "Creates SQL migration",
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := zap.L()
|
||||
|
||||
migrator := migrations.NewMigrator(nil)
|
||||
|
||||
files, err := migrator.CreateSQLMigrations(c.Context, strings.Join(c.Args().Slice(), "_"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrator.CreateSQLMigrations: %w", err)
|
||||
}
|
||||
|
||||
for _, mf := range files {
|
||||
logger.Info("created migration", zap.String("name", mf.Name), zap.String("path", mf.Path))
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newStatusCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "status",
|
||||
Usage: "Prints migrations status",
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := zap.L()
|
||||
|
||||
db, err := internal.NewBunDB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("internal.NewBunDB: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = db.Close()
|
||||
}()
|
||||
|
||||
migrator := migrations.NewMigrator(db)
|
||||
|
||||
ms, err := migrator.MigrationsWithStatus(c.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrator.MigrationsWithStatus: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("migrations: " + ms.String())
|
||||
logger.Info("last migration group: " + ms.LastGroup().String())
|
||||
logger.Info("unapplied: " + ms.Unapplied().String())
|
||||
|
||||
return nil
|
||||
newMigrationCmd(),
|
||||
newUserCmd(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/cmd/sessions/internal"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/bundb/migrations"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func newMigrationCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "migration",
|
||||
Usage: "Manages database migrations",
|
||||
Subcommands: []*cli.Command{
|
||||
newMigrateCmd(),
|
||||
newRollbackCmd(),
|
||||
newCreateMigrationCmd(),
|
||||
newStatusCmd(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newMigrateCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "migrate",
|
||||
Usage: "Migrates database",
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := zap.L()
|
||||
|
||||
db, err := internal.NewBunDB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("internal.NewBunDB: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = db.Close()
|
||||
}()
|
||||
|
||||
migrator := migrations.NewMigrator(db)
|
||||
|
||||
if err = migrator.Init(c.Context); err != nil {
|
||||
return fmt.Errorf("migrator.Init: %w", err)
|
||||
}
|
||||
|
||||
group, err := migrator.Migrate(c.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrator.Migrate: %w", err)
|
||||
}
|
||||
if group.ID == 0 {
|
||||
logger.Info("there are no new migrations to run")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Info("migrated to "+strconv.FormatInt(group.ID, 10), zap.Int64("id", group.ID))
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newRollbackCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "rollback",
|
||||
Usage: "Rollbacks the last migration group",
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := zap.L()
|
||||
|
||||
db, err := internal.NewBunDB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("internal.NewBunDB: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = db.Close()
|
||||
}()
|
||||
|
||||
migrator := migrations.NewMigrator(db)
|
||||
|
||||
group, err := migrator.Rollback(c.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrator.Rollback: %w", err)
|
||||
}
|
||||
if group.ID == 0 {
|
||||
logger.Info("there are no groups to roll back")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Info("rolled back "+strconv.FormatInt(group.ID, 10), zap.Int64("id", group.ID))
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newCreateMigrationCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "create",
|
||||
Usage: "Creates a migration",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "go",
|
||||
Usage: "Creates a Go migration",
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := zap.L()
|
||||
|
||||
migrator := migrations.NewMigrator(nil)
|
||||
|
||||
mf, err := migrator.CreateGoMigration(c.Context, strings.Join(c.Args().Slice(), "_"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrator.CreateGoMigration: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("created migration", zap.String("name", mf.Name), zap.String("path", mf.Path))
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "sql",
|
||||
Usage: "Creates an SQL migration",
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := zap.L()
|
||||
|
||||
migrator := migrations.NewMigrator(nil)
|
||||
|
||||
files, err := migrator.CreateSQLMigrations(c.Context, strings.Join(c.Args().Slice(), "_"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrator.CreateSQLMigrations: %w", err)
|
||||
}
|
||||
|
||||
for _, mf := range files {
|
||||
logger.Info("created migration", zap.String("name", mf.Name), zap.String("path", mf.Path))
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newStatusCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "status",
|
||||
Usage: "Prints migrations status",
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := zap.L()
|
||||
|
||||
db, err := internal.NewBunDB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("internal.NewBunDB: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = db.Close()
|
||||
}()
|
||||
|
||||
migrator := migrations.NewMigrator(db)
|
||||
|
||||
ms, err := migrator.MigrationsWithStatus(c.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrator.MigrationsWithStatus: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("migrations: " + ms.String())
|
||||
logger.Info("last migration group: " + ms.LastGroup().String())
|
||||
logger.Info("unapplied: " + ms.Unapplied().String())
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/cmd/sessions/internal"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/bundb"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/service"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
createUserTimeout = time.Second
|
||||
)
|
||||
|
||||
func newUserCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "user",
|
||||
Usage: "Manages users",
|
||||
Subcommands: []*cli.Command{
|
||||
newCreateUserCmd(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newCreateUserCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "create",
|
||||
Usage: "Creates a user",
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := zap.L()
|
||||
|
||||
db, err := internal.NewBunDB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("internal.NewBunDB: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = db.Close()
|
||||
}()
|
||||
|
||||
params, err := domain.NewCreateUserParams(c.String("name"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Context, createUserTimeout)
|
||||
defer cancel()
|
||||
|
||||
user, err := service.NewUser(bundb.NewUser(db)).Create(ctx, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UserService.Create: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("user created", zap.Int64("id", user.ID), zap.String("name", user.Name))
|
||||
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
2
go.mod
2
go.mod
|
@ -8,10 +8,12 @@ require (
|
|||
github.com/go-chi/chi/v5 v5.0.7
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/ory/dockertest/v3 v3.9.1
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/uptrace/bun v1.1.8
|
||||
github.com/uptrace/bun/dbfixture v1.1.8
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.1.8
|
||||
github.com/uptrace/bun/driver/pgdriver v1.1.8
|
||||
github.com/urfave/cli/v2 v2.23.5
|
||||
|
|
4
go.sum
4
go.sum
|
@ -54,6 +54,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
|||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw=
|
||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||
|
@ -110,6 +112,8 @@ github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYm
|
|||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
|
||||
github.com/uptrace/bun v1.1.8 h1:slxuaP4LYWFbPRUmTtQhfJN+6eX/6ar2HDKYTcI50SA=
|
||||
github.com/uptrace/bun v1.1.8/go.mod h1:iT89ESdV3uMupD9ixt6Khidht+BK0STabK/LeZE+B84=
|
||||
github.com/uptrace/bun/dbfixture v1.1.8 h1:cDRqII+emIgmmhWSebdAZWu5vKmuIw+PZ/arf0oYJdo=
|
||||
github.com/uptrace/bun/dbfixture v1.1.8/go.mod h1:GrRDt9lIfDpMyAGIJjWNsYeRHKjr8Gr2wDl2Rsf1sR4=
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.1.8 h1:wayJhjYDPGv8tgOBLolbBtSFQ0TihFoo8E1T129UdA8=
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.1.8/go.mod h1:nNbU8PHTjTUM+CRtGmqyBb9zcuRAB8I680/qoFSmBUk=
|
||||
github.com/uptrace/bun/driver/pgdriver v1.1.8 h1:gyL22axRQfjJS2Umq0erzJnp0bLOdUE8/USKZHPQB8o=
|
||||
|
|
|
@ -13,7 +13,9 @@ import (
|
|||
"time"
|
||||
"unicode"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/bundb/internal/model"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/bundb/migrations"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/google/uuid"
|
||||
"github.com/ory/dockertest/v3"
|
||||
|
@ -21,6 +23,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dbfixture"
|
||||
"github.com/uptrace/bun/dialect/pgdialect"
|
||||
"github.com/uptrace/bun/driver/pgdriver"
|
||||
)
|
||||
|
@ -126,6 +129,29 @@ func runMigrations(tb testing.TB, db *bun.DB) {
|
|||
require.NoError(tb, err, "couldn't migrate (2)")
|
||||
}
|
||||
|
||||
type bunfixture struct {
|
||||
*dbfixture.Fixture
|
||||
}
|
||||
|
||||
func loadFixtures(tb testing.TB, bunDB *bun.DB) *bunfixture {
|
||||
tb.Helper()
|
||||
|
||||
fixture := dbfixture.New(bunDB)
|
||||
err := fixture.Load(context.Background(), os.DirFS("testdata"), "fixture.yml")
|
||||
require.NoError(tb, err, "couldn't load fixtures")
|
||||
return &bunfixture{fixture}
|
||||
}
|
||||
|
||||
func (f *bunfixture) user(tb testing.TB, id string) domain.User {
|
||||
tb.Helper()
|
||||
|
||||
row, err := f.Row("User." + id)
|
||||
require.NoError(tb, err)
|
||||
g, ok := row.(*model.User)
|
||||
require.True(tb, ok)
|
||||
return g.ToDomain()
|
||||
}
|
||||
|
||||
func generateSchema() string {
|
||||
return strings.TrimFunc(strings.ReplaceAll(uuid.NewString(), "-", "_"), unicode.IsNumber)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ApiKey struct {
|
||||
bun.BaseModel `bun:"base_model,table:api_keys,alias:ak"`
|
||||
|
||||
ID int64 `bun:"id,pk,autoincrement,identity"`
|
||||
Key string `bun:"key,nullzero,type:varchar(255),notnull,unique"`
|
||||
UserID int64 `bun:"user_id,nullzero,notnull"`
|
||||
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
@ -11,8 +12,23 @@ type User struct {
|
|||
bun.BaseModel `bun:"base_model,table:users,alias:user"`
|
||||
|
||||
ID int64 `bun:"id,pk,autoincrement,identity"`
|
||||
Name string `bun:"name,type:varchar(255),notnull"`
|
||||
NameLower string `bun:"name_lower,type:varchar(255),notnull,unique"`
|
||||
ApiKey uuid.UUID `bun:"api_key,type:uuid,unique"`
|
||||
Name string `bun:"name,nullzero,type:varchar(255),notnull"`
|
||||
NameLower string `bun:"name_lower,nullzero,type:varchar(255),notnull,unique"`
|
||||
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
|
||||
}
|
||||
|
||||
func NewUser(p domain.CreateUserParams) User {
|
||||
return User{
|
||||
Name: p.Name(),
|
||||
NameLower: strings.ToLower(p.Name()),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (u User) ToDomain() domain.User {
|
||||
return domain.User{
|
||||
ID: u.ID,
|
||||
Name: u.Name,
|
||||
CreatedAt: u.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package model_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/bundb/internal/model"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var id int64 = 123
|
||||
params, err := domain.NewCreateUserParams("NAMEname")
|
||||
assert.NoError(t, err)
|
||||
|
||||
result := model.NewUser(params)
|
||||
result.ID = id
|
||||
assert.Equal(t, strings.ToLower(params.Name()), result.NameLower)
|
||||
|
||||
user := result.ToDomain()
|
||||
assert.Equal(t, id, user.ID)
|
||||
assert.Equal(t, params.Name(), user.Name)
|
||||
assert.WithinDuration(t, time.Now(), user.CreatedAt, 10*time.Millisecond)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
- model: User
|
||||
rows:
|
||||
- _id: user-1
|
||||
id: 11111
|
||||
name: User-1
|
||||
name_lower: user-1
|
||||
created_at: 2022-03-15T15:00:10.000Z
|
|
@ -0,0 +1,55 @@
|
|||
package bundb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/bundb/internal/model"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/driver/pgdriver"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
db *bun.DB
|
||||
}
|
||||
|
||||
func NewUser(db *bun.DB) *User {
|
||||
return &User{db: db}
|
||||
}
|
||||
|
||||
func (u *User) Create(ctx context.Context, params domain.CreateUserParams) (domain.User, error) {
|
||||
user := model.NewUser(params)
|
||||
|
||||
if _, err := u.db.NewInsert().
|
||||
Model(&user).
|
||||
Returning("*").
|
||||
Exec(ctx); err != nil {
|
||||
return domain.User{}, fmt.Errorf(
|
||||
"something went wrong while inserting user into the db: %w",
|
||||
mapCreateUserError(err, params),
|
||||
)
|
||||
}
|
||||
|
||||
return user.ToDomain(), nil
|
||||
}
|
||||
|
||||
func mapCreateUserError(err error, params domain.CreateUserParams) error {
|
||||
var pgError pgdriver.Error
|
||||
if !errors.As(err, &pgError) {
|
||||
return err
|
||||
}
|
||||
|
||||
code := pgError.Field('C')
|
||||
constraint := pgError.Field('n')
|
||||
switch {
|
||||
case code == pgerrcode.UniqueViolation && constraint == "users_name_lower_key":
|
||||
return domain.UsernameAlreadyTakenError{
|
||||
Name: params.Name(),
|
||||
}
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package bundb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/bundb"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUser_Create(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := newDB(t)
|
||||
fixture := loadFixtures(t, db)
|
||||
repo := bundb.NewUser(db)
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params, err := domain.NewCreateUserParams("nameName")
|
||||
require.Nil(t, err)
|
||||
|
||||
user, err := repo.Create(context.Background(), params)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, user.ID, int64(0))
|
||||
assert.Equal(t, params.Name(), user.Name)
|
||||
assert.WithinDuration(t, time.Now(), user.CreatedAt, time.Second)
|
||||
})
|
||||
|
||||
t.Run("ERR: username must be unique", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
name := fixture.user(t, "user-1").Name
|
||||
names := []string{
|
||||
name,
|
||||
strings.ToUpper(name),
|
||||
strings.ToLower(name),
|
||||
}
|
||||
|
||||
for _, n := range names {
|
||||
n := n
|
||||
|
||||
t.Run(n, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params, err := domain.NewCreateUserParams(n)
|
||||
require.NoError(t, err)
|
||||
|
||||
user, err := repo.Create(context.Background(), params)
|
||||
assert.ErrorIs(t, err, domain.UsernameAlreadyTakenError{
|
||||
Name: n,
|
||||
})
|
||||
assert.Zero(t, user)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRequired = errors.New("cannot be blank")
|
||||
)
|
||||
|
||||
type ErrorCode uint8
|
||||
|
||||
const (
|
||||
ErrorCodeUnknown ErrorCode = iota
|
||||
ErrorCodeEntityNotFound
|
||||
ErrorCodeValidationError
|
||||
ErrorCodeAlreadyExists
|
||||
)
|
||||
|
||||
func (e ErrorCode) String() string {
|
||||
switch e {
|
||||
case ErrorCodeEntityNotFound:
|
||||
return "entity-not-found"
|
||||
case ErrorCodeValidationError:
|
||||
return "validation-error"
|
||||
case ErrorCodeAlreadyExists:
|
||||
return "already-exists"
|
||||
case ErrorCodeUnknown:
|
||||
fallthrough
|
||||
default:
|
||||
return "internal-server-error"
|
||||
}
|
||||
}
|
||||
|
||||
type Error interface {
|
||||
error
|
||||
UserError() string
|
||||
Code() ErrorCode
|
||||
}
|
||||
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e ValidationError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.Field, e.Err)
|
||||
}
|
||||
|
||||
func (e ValidationError) UserError() string {
|
||||
return e.Error()
|
||||
}
|
||||
|
||||
func (e ValidationError) Code() ErrorCode {
|
||||
return ErrorCodeValidationError
|
||||
}
|
||||
|
||||
func (e ValidationError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
type MinLengthError struct {
|
||||
Min int
|
||||
}
|
||||
|
||||
func (e MinLengthError) Error() string {
|
||||
return fmt.Sprintf("the length must be no less than %d", e.Min)
|
||||
}
|
||||
|
||||
func (e MinLengthError) UserError() string {
|
||||
return e.Error()
|
||||
}
|
||||
|
||||
func (e MinLengthError) Code() ErrorCode {
|
||||
return ErrorCodeValidationError
|
||||
}
|
||||
|
||||
type MaxLengthError struct {
|
||||
Max int
|
||||
}
|
||||
|
||||
func (e MaxLengthError) Error() string {
|
||||
return fmt.Sprintf("the length must be no more than %d", e.Max)
|
||||
}
|
||||
|
||||
func (e MaxLengthError) UserError() string {
|
||||
return e.Error()
|
||||
}
|
||||
|
||||
func (e MaxLengthError) Code() ErrorCode {
|
||||
return ErrorCodeValidationError
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package domain_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestErrorCode_String(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Equal(t, domain.ErrorCodeUnknown.String(), "internal-server-error")
|
||||
assert.Equal(t, domain.ErrorCodeEntityNotFound.String(), "entity-not-found")
|
||||
assert.Equal(t, domain.ErrorCodeValidationError.String(), "validation-error")
|
||||
}
|
||||
|
||||
func TestValidationError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := domain.ValidationError{
|
||||
Field: "test",
|
||||
Err: domain.ErrRequired,
|
||||
}
|
||||
var _ domain.Error = err
|
||||
assert.Equal(t, fmt.Sprintf("%s: %s", err.Field, err.Err.Error()), err.Error())
|
||||
assert.Equal(t, err.Error(), err.UserError())
|
||||
assert.ErrorIs(t, err, err.Err)
|
||||
assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
|
||||
}
|
||||
|
||||
func TestMinLengthError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := domain.MinLengthError{
|
||||
Min: 25,
|
||||
}
|
||||
var _ domain.Error = err
|
||||
assert.Equal(t, "the length must be no less than 25", err.Error())
|
||||
assert.Equal(t, err.Error(), err.UserError())
|
||||
assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
|
||||
}
|
||||
|
||||
func TestMaxLengthError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := domain.MaxLengthError{
|
||||
Max: 25,
|
||||
}
|
||||
var _ domain.Error = err
|
||||
assert.Equal(t, "the length must be no more than 25", err.Error())
|
||||
assert.Equal(t, err.Error(), err.UserError())
|
||||
assert.Equal(t, domain.ErrorCodeValidationError, err.Code())
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
UsernameMinLength = 2
|
||||
UsernameMaxLength = 40
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUsernameFormat = errors.New("may only contain alphanumeric characters or single hyphens and cannot begin or end with a hyphen")
|
||||
|
||||
rxAlphanumericHyphens = regexp.MustCompile("^[a-zA-Z0-9]+([-][a-zA-Z0-9]+)*$")
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int64
|
||||
Name string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type CreateUserParams struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func NewCreateUserParams(name string) (CreateUserParams, error) {
|
||||
if name == "" {
|
||||
return CreateUserParams{}, ValidationError{
|
||||
Field: "Name",
|
||||
Err: ErrRequired,
|
||||
}
|
||||
}
|
||||
|
||||
if len(name) < UsernameMinLength {
|
||||
return CreateUserParams{}, ValidationError{
|
||||
Field: "Name",
|
||||
Err: MinLengthError{
|
||||
Min: UsernameMinLength,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if len(name) > UsernameMaxLength {
|
||||
return CreateUserParams{}, ValidationError{
|
||||
Field: "Name",
|
||||
Err: MaxLengthError{
|
||||
Max: UsernameMaxLength,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if !rxAlphanumericHyphens.MatchString(name) {
|
||||
return CreateUserParams{}, ValidationError{
|
||||
Field: "Name",
|
||||
Err: ErrUsernameFormat,
|
||||
}
|
||||
}
|
||||
|
||||
return CreateUserParams{
|
||||
name: name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c CreateUserParams) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
type UsernameAlreadyTakenError struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (e UsernameAlreadyTakenError) Error() string {
|
||||
return fmt.Sprintf("username '%s' is already taken", e.Name)
|
||||
}
|
||||
|
||||
func (e UsernameAlreadyTakenError) UserError() string {
|
||||
return e.Error()
|
||||
}
|
||||
|
||||
func (e UsernameAlreadyTakenError) Code() ErrorCode {
|
||||
return ErrorCodeAlreadyExists
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package domain_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCreateUserParams(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
username string
|
||||
apiKey string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "OK",
|
||||
username: "testTEST-test-2",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "ERR: username is required",
|
||||
username: "",
|
||||
expectedErr: domain.ValidationError{
|
||||
Field: "Name",
|
||||
Err: domain.ErrRequired,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: len(username) < 2",
|
||||
username: "u",
|
||||
expectedErr: domain.ValidationError{
|
||||
Field: "Name",
|
||||
Err: domain.MinLengthError{
|
||||
Min: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: username - hyphen at the end",
|
||||
username: "u-",
|
||||
expectedErr: domain.ValidationError{
|
||||
Field: "Name",
|
||||
Err: domain.ErrUsernameFormat,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: username - hyphen at the beginning",
|
||||
username: "-u",
|
||||
expectedErr: domain.ValidationError{
|
||||
Field: "Name",
|
||||
Err: domain.ErrUsernameFormat,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: username - only alphanumeric 1",
|
||||
username: "userńame",
|
||||
expectedErr: domain.ValidationError{
|
||||
Field: "Name",
|
||||
Err: domain.ErrUsernameFormat,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: username - only alphanumeric 1",
|
||||
username: "漢字",
|
||||
expectedErr: domain.ValidationError{
|
||||
Field: "Name",
|
||||
Err: domain.ErrUsernameFormat,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params, err := domain.NewCreateUserParams(tt.username)
|
||||
if tt.expectedErr != nil {
|
||||
assert.Equal(t, tt.expectedErr, err)
|
||||
assert.Zero(t, params)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.username, params.Name())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsernameAlreadyTakenError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := domain.UsernameAlreadyTakenError{
|
||||
Name: "xxxx",
|
||||
}
|
||||
var _ domain.Error = err
|
||||
assert.Equal(t, fmt.Sprintf("username '%s' is already taken", err.Name), err.Error())
|
||||
assert.Equal(t, err.Error(), err.UserError())
|
||||
assert.Equal(t, domain.ErrorCodeAlreadyExists, err.Code())
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
Create(ctx context.Context, params domain.CreateUserParams) (domain.User, error)
|
||||
}
|
||||
|
||||
type User struct {
|
||||
repo UserRepository
|
||||
}
|
||||
|
||||
func NewUser(repo UserRepository) *User {
|
||||
return &User{repo: repo}
|
||||
}
|
||||
|
||||
func (u *User) Create(ctx context.Context, params domain.CreateUserParams) (domain.User, error) {
|
||||
user, err := u.repo.Create(ctx, params)
|
||||
if err != nil {
|
||||
return domain.User{}, fmt.Errorf("UserRepository.Create: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
|
@ -9,7 +9,7 @@ spec:
|
|||
containers:
|
||||
- name: sessions-migrations
|
||||
image: sessions
|
||||
args: ["db", "migrate"]
|
||||
args: ["db", "migration", "migrate"]
|
||||
env:
|
||||
- name: APP_MODE
|
||||
value: development
|
||||
|
|
Loading…
Reference in New Issue