diff --git a/README.md b/README.md index 376f95b..c51daa8 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The following feature set is a minimal selection of typical Web API requirements - Configuration using [viper](https://github.com/spf13/viper) - CLI features using [cobra](https://github.com/spf13/cobra) -- PostgreSQL support including migrations using [go-pg](https://github.com/go-pg/pg) +- PostgreSQL support including migrations using [bun](https://github.com/uptrace/bun) - Structured logging with [Logrus](https://github.com/sirupsen/logrus) - Routing with [chi router](https://github.com/go-chi/chi) and middleware - JWT Authentication using [lestrrat-go/jwx](https://github.com/lestrrat-go/jwx) with example passwordless email authentication @@ -35,8 +35,8 @@ The following feature set is a minimal selection of typical Web API requirements ### Using Docker Compose - First start the database only: `docker compose up -d postgres` -- Once initialize the database by running all migrations in database/migrate folder: `docker compose exec server ./main migrate` -- Start the api server: `docker compose up server` +- Once initialize the database by running all migrations in database/migrate folder: `docker compose run server ./main migrate` +- Start the api server: `docker compose up` ## API Routes @@ -53,7 +53,7 @@ For passwordless login following routes are available: ### Example API -Besides /auth/_ the API provides two main routes /api/_ and /admin/\*, as an example to separate application and administration context. The latter requires to be logged in as administrator by providing the respective JWT in Authorization Header. +Besides /auth/_the API provides two main routes /api/_ and /admin/\*, as an example to separate application and administration context. The latter requires to be logged in as administrator by providing the respective JWT in Authorization Header. Check [routes.md](routes.md) for a generated overview of the provided API routes. @@ -71,9 +71,10 @@ Outgoing emails containing the login token will be print to stdout if no valid e Use one of the following bootstrapped users for login: -- admin@boot.io (has access to admin panel) -- user@boot.io +- (has access to admin panel) +- +TODO: deploy somewhere else... A deployed version can also be found on [Heroku](https://govue.herokuapp.com) ### Testing @@ -94,7 +95,7 @@ By default viper will look at $HOME/.go-base.yaml for a config file. Setting you | DB_USER | string | postgres | database user name | | DB_PASSWORD | string | postgres | database user password | | DB_DATABASE | string | postgres | database shema name | -| AUTH_LOGIN_URL | string | http://localhost:3000/login | client login url as sent in login token email | +| AUTH_LOGIN_URL | string | | client login url as sent in login token email | | AUTH_LOGIN_TOKEN_LENGTH | int | 8 | length of login token | | AUTH_LOGIN_TOKEN_EXPIRY | time.Duration | 11m | login token expiry | | AUTH_JWT_SECRET | string | random | jwt sign and verify key - value "random" creates random 32 char secret at startup (and automatically invalidates existing tokens on app restarts, so during dev you might want to set a fixed value here) | diff --git a/api/admin/api.go b/api/admin/api.go index c19621f..75b4738 100644 --- a/api/admin/api.go +++ b/api/admin/api.go @@ -5,9 +5,9 @@ import ( "net/http" "github.com/sirupsen/logrus" + "github.com/uptrace/bun" "github.com/go-chi/chi/v5" - "github.com/go-pg/pg" "github.com/dhax/go-base/auth/authorize" "github.com/dhax/go-base/database" @@ -30,8 +30,7 @@ type API struct { } // NewAPI configures and returns admin application API. -func NewAPI(db *pg.DB) (*API, error) { - +func NewAPI(db *bun.DB) (*API, error) { accountStore := database.NewAdmAccountStore(db) accounts := NewAccountResource(accountStore) diff --git a/api/api.go b/api/api.go index 0493735..1c47655 100644 --- a/api/api.go +++ b/api/api.go @@ -82,8 +82,7 @@ func New(enableCORS bool) (*chi.Mux, error) { w.Write([]byte("pong")) }) - client := "./public" - r.Get("/*", SPAHandler(client)) + r.Get("/*", SPAHandler("public")) return r, nil } diff --git a/api/app/api.go b/api/app/api.go index 3f5c734..d872013 100644 --- a/api/app/api.go +++ b/api/app/api.go @@ -5,8 +5,8 @@ import ( "net/http" "github.com/go-chi/chi/v5" - "github.com/go-pg/pg" "github.com/sirupsen/logrus" + "github.com/uptrace/bun" "github.com/dhax/go-base/database" "github.com/dhax/go-base/logging" @@ -26,7 +26,7 @@ type API struct { } // NewAPI configures and returns application API. -func NewAPI(db *pg.DB) (*API, error) { +func NewAPI(db *bun.DB) (*API, error) { accountStore := database.NewAccountStore(db) account := NewAccountResource(accountStore) diff --git a/auth/jwt/token.go b/auth/jwt/token.go index ffb20ff..1df1a78 100644 --- a/auth/jwt/token.go +++ b/auth/jwt/token.go @@ -3,24 +3,24 @@ package jwt import ( "time" - "github.com/go-pg/pg/orm" + "github.com/uptrace/bun" ) // Token holds refresh jwt information. type Token struct { - ID int `json:"id,omitempty"` - CreatedAt time.Time `json:"created_at,omitempty"` - UpdatedAt time.Time `json:"updated_at,omitempty"` - AccountID int `json:"-"` + ID int `bun:"id,pk,autoincrement" json:"id,omitempty"` + CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp" json:"created_at,omitempty"` + UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp" json:"updated_at,omitempty"` + AccountID int `bun:"account_id,notnull" json:"-"` - Token string `json:"-"` - Expiry time.Time `json:"-"` - Mobile bool `sql:",notnull" json:"mobile"` - Identifier string `json:"identifier,omitempty"` + Token string `bun:"token,notnull" json:"-"` + Expiry time.Time `bun:"expiry,notnull" json:"-"` + Mobile bool `bun:"mobile,notnull" json:"mobile"` + Identifier string `bun:"identifier" json:"identifier,omitempty"` } // BeforeInsert hook executed before database insert operation. -func (t *Token) BeforeInsert(db orm.DB) error { +func (t *Token) BeforeInsert(db *bun.DB) error { now := time.Now() if t.CreatedAt.IsZero() { t.CreatedAt = now @@ -30,7 +30,7 @@ func (t *Token) BeforeInsert(db orm.DB) error { } // BeforeUpdate hook executed before database update operation. -func (t *Token) BeforeUpdate(db orm.DB) error { +func (t *Token) BeforeUpdate(db *bun.DB) error { t.UpdatedAt = time.Now() return nil } diff --git a/auth/pwdless/account.go b/auth/pwdless/account.go index 95fa756..b47fa5d 100644 --- a/auth/pwdless/account.go +++ b/auth/pwdless/account.go @@ -6,28 +6,28 @@ import ( validation "github.com/go-ozzo/ozzo-validation" "github.com/go-ozzo/ozzo-validation/is" - "github.com/go-pg/pg/orm" + "github.com/uptrace/bun" "github.com/dhax/go-base/auth/jwt" ) // Account represents an authenticated application user type Account struct { - ID int `json:"id"` - CreatedAt time.Time `json:"created_at,omitempty"` - UpdatedAt time.Time `json:"updated_at,omitempty"` - LastLogin time.Time `json:"last_login,omitempty"` + ID int `bun:"id,pk,autoincrement" json:"id"` + CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp" json:"created_at,omitempty"` + UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp" json:"updated_at,omitempty"` + LastLogin time.Time `bun:"last_login" json:"last_login,omitempty"` - Email string `json:"email"` - Name string `json:"name"` - Active bool `sql:",notnull" json:"active"` - Roles []string `pg:",array" json:"roles,omitempty"` + Email string `bun:"email,notnull" json:"email"` + Name string `bun:"name,notnull" json:"name"` + Active bool `bun:"active,notnull" json:"active"` + Roles []string `bun:"roles,array" json:"roles,omitempty"` - Token []jwt.Token `json:"token,omitempty"` + Token []jwt.Token `bun:"rel:has-many" json:"token,omitempty"` } // BeforeInsert hook executed before database insert operation. -func (a *Account) BeforeInsert(db orm.DB) error { +func (a *Account) BeforeInsert(db *bun.DB) error { now := time.Now() if a.CreatedAt.IsZero() { a.CreatedAt = now @@ -37,13 +37,13 @@ func (a *Account) BeforeInsert(db orm.DB) error { } // BeforeUpdate hook executed before database update operation. -func (a *Account) BeforeUpdate(db orm.DB) error { +func (a *Account) BeforeUpdate(db *bun.DB) error { a.UpdatedAt = time.Now() return a.Validate() } // BeforeDelete hook executed before database delete operation. -func (a *Account) BeforeDelete(db orm.DB) error { +func (a *Account) BeforeDelete(db *bun.DB) error { return nil } diff --git a/cmd/migrate.go b/cmd/migrate.go index 83d7425..58b7eda 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -3,30 +3,16 @@ package cmd import ( "github.com/spf13/cobra" - "github.com/dhax/go-base/database/migrate" + "github.com/dhax/go-base/database/migrations" ) -var reset bool - // migrateCmd represents the migrate command var migrateCmd = &cobra.Command{ Use: "migrate", - Short: "use go-pg migration tool", - Long: `migrate uses go-pg migration tool under the hood supporting the same commands and an additional reset command`, + Short: "use bun migration tool", + Long: `run bun migrations`, Run: func(cmd *cobra.Command, args []string) { - argsMig := args[:0] - for _, arg := range args { - switch arg { - case "migrate", "--db_debug", "--reset": - default: - argsMig = append(argsMig, arg) - } - } - - if reset { - migrate.Reset() - } - migrate.Migrate(argsMig) + migrations.Migrate() }, } @@ -42,5 +28,4 @@ func init() { // Cobra supports local flags which will only run when this command // is called directly, e.g.: // migrateCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") - migrateCmd.Flags().BoolVar(&reset, "reset", false, "migrate down to version 0 then up to latest. WARNING: all data will be lost!") } diff --git a/database/accountStore.go b/database/accountStore.go index 563634a..a7e3ab2 100644 --- a/database/accountStore.go +++ b/database/accountStore.go @@ -1,19 +1,22 @@ package database import ( + "context" + "database/sql" + "github.com/dhax/go-base/auth/jwt" "github.com/dhax/go-base/auth/pwdless" "github.com/dhax/go-base/models" - "github.com/go-pg/pg" + "github.com/uptrace/bun" ) // AccountStore implements database operations for account management by user. type AccountStore struct { - db *pg.DB + db *bun.DB } // NewAccountStore returns an AccountStore. -func NewAccountStore(db *pg.DB) *AccountStore { +func NewAccountStore(db *bun.DB) *AccountStore { return &AccountStore{ db: db, } @@ -21,52 +24,71 @@ func NewAccountStore(db *pg.DB) *AccountStore { // Get an account by ID. func (s *AccountStore) Get(id int) (*pwdless.Account, error) { - a := pwdless.Account{ID: id} - err := s.db.Model(&a). - Where("account.id = ?id"). - Column("account.*", "Token"). - First() - return &a, err + a := &pwdless.Account{ID: id} + err := s.db.NewSelect(). + Model(a). + Where("id = ?", id). + Relation("Token"). + Scan(context.Background()) + return a, err } // Update an account. func (s *AccountStore) Update(a *pwdless.Account) error { - _, err := s.db.Model(a). + _, err := s.db.NewUpdate(). + Model(a). Column("email", "name"). WherePK(). - Update() + Exec(context.Background()) return err } // Delete an account. func (s *AccountStore) Delete(a *pwdless.Account) error { - err := s.db.RunInTransaction(func(tx *pg.Tx) error { - if _, err := tx.Model(&jwt.Token{}). - Where("account_id = ?", a.ID). - Delete(); err != nil { - return err - } - if _, err := tx.Model(&models.Profile{}). - Where("account_id = ?", a.ID). - Delete(); err != nil { - return err - } - return tx.Delete(a) - }) - return err + ctx := context.Background() + tx, err := s.db.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + return err + } + if _, err := tx.NewDelete(). + Model((*jwt.Token)(nil)). + Where("account_id = ?", a.ID). + Exec(ctx); err != nil { + tx.Rollback() + return err + } + if _, err := tx.NewDelete(). + Model((*models.Profile)(nil)). + Where("account_id = ?", a.ID). + Exec(ctx); err != nil { + tx.Rollback() + return err + } + if _, err := tx.NewDelete(). + Model(a). + WherePK(). + Exec(ctx); err != nil { + tx.Rollback() + } + tx.Commit() + return nil } // UpdateToken updates a jwt refresh token. func (s *AccountStore) UpdateToken(t *jwt.Token) error { - _, err := s.db.Model(t). + _, err := s.db.NewUpdate(). + Model(t). Column("identifier"). WherePK(). - Update() + Exec(context.Background()) return err } // DeleteToken deletes a jwt refresh token. func (s *AccountStore) DeleteToken(t *jwt.Token) error { - err := s.db.Delete(t) + _, err := s.db.NewDelete(). + Model(t). + WherePK(). + Exec(context.Background()) return err } diff --git a/database/admAccountStore.go b/database/admAccountStore.go index 46aecbd..b9af692 100644 --- a/database/admAccountStore.go +++ b/database/admAccountStore.go @@ -1,15 +1,15 @@ package database import ( + "context" + "database/sql" "errors" "net/url" "github.com/dhax/go-base/auth/jwt" "github.com/dhax/go-base/auth/pwdless" "github.com/dhax/go-base/models" - "github.com/go-pg/pg" - "github.com/go-pg/pg/orm" - "github.com/go-pg/pg/urlvalues" + "github.com/uptrace/bun" ) var ( @@ -21,11 +21,11 @@ var ( // AdmAccountStore implements database operations for account management by admin. type AdmAccountStore struct { - db *pg.DB + db *bun.DB } // NewAdmAccountStore returns an AccountStore. -func NewAdmAccountStore(db *pg.DB) *AdmAccountStore { +func NewAdmAccountStore(db *bun.DB) *AdmAccountStore { return &AdmAccountStore{ db: db, } @@ -33,8 +33,9 @@ func NewAdmAccountStore(db *pg.DB) *AdmAccountStore { // AccountFilter provides pagination and filtering options on accounts. type AccountFilter struct { - Pager *urlvalues.Pager - Filter *urlvalues.Filter + Limit int + Offset int + Filter map[string]interface{} Order []string } @@ -44,29 +45,47 @@ func NewAccountFilter(params interface{}) (*AccountFilter, error) { if !ok { return nil, ErrBadParams } - p := urlvalues.Values(v) f := &AccountFilter{ - Pager: urlvalues.NewPager(p), - Filter: urlvalues.NewFilter(p), - Order: p["order"], + Limit: 10, // Default limit + Offset: 0, // Default offset + Filter: make(map[string]interface{}), + Order: v["order"], + } + // Parse limit and offset + if limit := v.Get("limit"); limit != "" { + f.Limit = int(limit[0] - '0') + } + if offset := v.Get("offset"); offset != "" { + f.Offset = int(offset[0] - '0') + } + // Parse filters + for key, values := range v { + if key != "limit" && key != "offset" && key != "order" { + f.Filter[key] = values[0] + } } return f, nil } -// Apply applies an AccountFilter on an orm.Query. -func (f *AccountFilter) Apply(q *orm.Query) (*orm.Query, error) { - q = q.Apply(f.Pager.Pagination) - q = q.Apply(f.Filter.Filters) - q = q.Order(f.Order...) - return q, nil +// Apply applies an AccountFilter on a bun.SelectQuery. +func (f *AccountFilter) Apply(q *bun.SelectQuery) *bun.SelectQuery { + q = q.Limit(f.Limit).Offset(f.Offset) + for key, value := range f.Filter { + q = q.Where("? = ?", bun.Ident(key), value) + } + for _, order := range f.Order { + q = q.Order(order) + } + return q } // List applies a filter and returns paginated array of matching results and total count. func (s *AdmAccountStore) List(f *AccountFilter) ([]pwdless.Account, int, error) { - a := []pwdless.Account{} - count, err := s.db.Model(&a). + var a []pwdless.Account + count, err := s.db.NewSelect(). + Model(&a). Apply(f.Apply). - SelectAndCount() + ScanAndCount(context.Background()) if err != nil { return nil, 0, err } @@ -75,55 +94,90 @@ func (s *AdmAccountStore) List(f *AccountFilter) ([]pwdless.Account, int, error) // Create creates a new account. func (s *AdmAccountStore) Create(a *pwdless.Account) error { - count, _ := s.db.Model(a). - Where("email = ?email"). - Count() - - if count != 0 { - return ErrUniqueEmailConstraint + exists, err := s.db.NewSelect(). + Model((*pwdless.Account)(nil)). + Where("email = ?", a.Email). + Exists(context.Background()) + if err != nil { + return err } - err := s.db.RunInTransaction(func(tx *pg.Tx) error { - err := tx.Insert(a) - if err != nil { - return err - } - p := &models.Profile{ - AccountID: a.ID, - } - return tx.Insert(p) - }) + if exists { + return ErrUniqueEmailConstraint + } + ctx := context.Background() + tx, err := s.db.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + return err + } + if _, err := tx.NewInsert(). + Model(a). + Exec(ctx); err != nil { + tx.Rollback() + return err + } + p := &models.Profile{ + AccountID: a.ID, + } + if _, err := tx.NewInsert(). + Model(p). + Exec(ctx); err != nil { + tx.Rollback() - return err + return err + } + tx.Commit() + + return nil } // Get account by ID. func (s *AdmAccountStore) Get(id int) (*pwdless.Account, error) { - a := pwdless.Account{ID: id} - err := s.db.Select(&a) - return &a, err + a := &pwdless.Account{ID: id} + err := s.db.NewSelect(). + Model(a). + WherePK(). + Scan(context.Background()) + return a, err } // Update account. func (s *AdmAccountStore) Update(a *pwdless.Account) error { - err := s.db.Update(a) + _, err := s.db.NewUpdate(). + Model(a). + WherePK(). + Exec(context.Background()) return err } // Delete account. func (s *AdmAccountStore) Delete(a *pwdless.Account) error { - err := s.db.RunInTransaction(func(tx *pg.Tx) error { - if _, err := tx.Model(&jwt.Token{}). - Where("account_id = ?", a.ID). - Delete(); err != nil { - return err - } - if _, err := tx.Model(&models.Profile{}). - Where("account_id = ?", a.ID). - Delete(); err != nil { - return err - } - return tx.Delete(a) - }) - return err + ctx := context.Background() + tx, err := s.db.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + return err + } + if _, err := tx.NewDelete(). + Model((*jwt.Token)(nil)). + Where("account_id = ?", a.ID). + Exec(ctx); err != nil { + tx.Rollback() + return err + } + if _, err := tx.NewDelete(). + Model((*models.Profile)(nil)). + Where("account_id = ?", a.ID). + Exec(ctx); err != nil { + tx.Rollback() + return err + } + if _, err := tx.NewDelete(). + Model(a). + WherePK(). + Exec(ctx); err != nil { + tx.Rollback() + return err + } + tx.Commit() + return nil } diff --git a/database/authStore.go b/database/authStore.go index ad4127d..ba47290 100644 --- a/database/authStore.go +++ b/database/authStore.go @@ -1,20 +1,21 @@ package database import ( + "context" "time" "github.com/dhax/go-base/auth/jwt" "github.com/dhax/go-base/auth/pwdless" - "github.com/go-pg/pg" + "github.com/uptrace/bun" ) // AuthStore implements database operations for account pwdlessentication. type AuthStore struct { - db *pg.DB + db *bun.DB } // NewAuthStore return an AuthStore. -func NewAuthStore(db *pg.DB) *AuthStore { +func NewAuthStore(db *bun.DB) *AuthStore { return &AuthStore{ db: db, } @@ -22,65 +23,74 @@ func NewAuthStore(db *pg.DB) *AuthStore { // GetAccount returns an account by ID. func (s *AuthStore) GetAccount(id int) (*pwdless.Account, error) { - a := pwdless.Account{ID: id} - err := s.db.Model(&a). - Column("account.*"). - Where("id = ?id"). - First() - return &a, err + a := &pwdless.Account{ID: id} + err := s.db.NewSelect(). + Model(a). + Where("id = ?", id). + Scan(context.Background()) + return a, err } // GetAccountByEmail returns an account by email. func (s *AuthStore) GetAccountByEmail(e string) (*pwdless.Account, error) { - a := pwdless.Account{Email: e} - err := s.db.Model(&a). + a := &pwdless.Account{Email: e} + err := s.db.NewSelect(). + Model(a). Column("id", "active", "email", "name"). - Where("email = ?email"). - First() - return &a, err + Where("email = ?", e). + Scan(context.Background()) + return a, err } // UpdateAccount upates account data related to pwdlessentication. func (s *AuthStore) UpdateAccount(a *pwdless.Account) error { - _, err := s.db.Model(a). + _, err := s.db.NewUpdate(). + Model(a). Column("last_login"). WherePK(). - Update() + Exec(context.Background()) return err } // GetToken returns refresh token by token identifier. func (s *AuthStore) GetToken(t string) (*jwt.Token, error) { - token := jwt.Token{Token: t} - err := s.db.Model(&token). - Where("token = ?token"). - First() - - return &token, err + token := &jwt.Token{Token: t} + err := s.db.NewSelect(). + Model(token). + Where("token = ?", t). + Scan(context.Background()) + return token, err } // CreateOrUpdateToken creates or updates an existing refresh token. func (s *AuthStore) CreateOrUpdateToken(t *jwt.Token) error { - var err error if t.ID == 0 { - err = s.db.Insert(t) - } else { - err = s.db.Update(t) + _, err := s.db.NewInsert(). + Model(t). + Exec(context.Background()) + return err } + _, err := s.db.NewUpdate(). + Model(t). + WherePK(). + Exec(context.Background()) return err } // DeleteToken deletes a refresh token. func (s *AuthStore) DeleteToken(t *jwt.Token) error { - err := s.db.Delete(t) + _, err := s.db.NewDelete(). + Model(t). + WherePK(). + Exec(context.Background()) return err } // PurgeExpiredToken deletes expired refresh token. func (s *AuthStore) PurgeExpiredToken() error { - _, err := s.db.Model(&jwt.Token{}). + _, err := s.db.NewDelete(). + Model((*jwt.Token)(nil)). Where("expiry < ?", time.Now()). - Delete() - + Exec(context.Background()) return err } diff --git a/database/migrate/2_bootstrap_users.go b/database/migrate/2_bootstrap_users.go deleted file mode 100644 index 839348c..0000000 --- a/database/migrate/2_bootstrap_users.go +++ /dev/null @@ -1,48 +0,0 @@ -package migrate - -import ( - "fmt" - - "github.com/go-pg/migrations" -) - -const bootstrapAdminAccount = ` -INSERT INTO accounts (id, email, name, active, roles) -VALUES (DEFAULT, 'admin@boot.io', 'Admin Boot', true, '{admin}') -` - -const bootstrapUserAccount = ` -INSERT INTO accounts (id, email, name, active) -VALUES (DEFAULT, 'user@boot.io', 'User Boot', true) -` - -func init() { - up := []string{ - bootstrapAdminAccount, - bootstrapUserAccount, - } - - down := []string{ - `TRUNCATE accounts CASCADE`, - } - - migrations.Register(func(db migrations.DB) error { - fmt.Println("add bootstrap accounts") - for _, q := range up { - _, err := db.Exec(q) - if err != nil { - return err - } - } - return nil - }, func(db migrations.DB) error { - fmt.Println("truncate accounts cascading") - for _, q := range down { - _, err := db.Exec(q) - if err != nil { - return err - } - } - return nil - }) -} diff --git a/database/migrate/main.go b/database/migrate/main.go deleted file mode 100644 index ae1930e..0000000 --- a/database/migrate/main.go +++ /dev/null @@ -1,63 +0,0 @@ -// Package migrate implements postgres migrations. -package migrate - -import ( - "log" - - "github.com/dhax/go-base/database" - "github.com/go-pg/migrations" - "github.com/go-pg/pg" -) - -// Migrate runs go-pg migrations -func Migrate(args []string) { - db, err := database.DBConn() - if err != nil { - log.Fatal(err) - } - - err = db.RunInTransaction(func(tx *pg.Tx) error { - oldVersion, newVersion, err := migrations.Run(tx, args...) - if err != nil { - return err - } - if newVersion != oldVersion { - log.Printf("migrated from version %d to %d\n", oldVersion, newVersion) - } else { - log.Printf("version is %d\n", oldVersion) - } - return nil - }) - if err != nil { - log.Fatal(err) - } - -} - -// Reset runs reverts all migrations to version 0 and then applies all migrations to latest -func Reset() { - db, err := database.DBConn() - if err != nil { - log.Fatal(err) - } - - version, err := migrations.Version(db) - if err != nil { - log.Fatal(err) - } - - err = db.RunInTransaction(func(tx *pg.Tx) error { - for version != 0 { - oldVersion, newVersion, err := migrations.Run(tx, "down") - if err != nil { - return err - } - log.Printf("migrated from version %d to %d\n", oldVersion, newVersion) - version = newVersion - } - return nil - }) - if err != nil { - log.Fatal(err) - } -} diff --git a/database/migrate/1_initial.go b/database/migrations/1_initial.go similarity index 87% rename from database/migrate/1_initial.go rename to database/migrations/1_initial.go index 2952d4e..110f6ec 100644 --- a/database/migrate/1_initial.go +++ b/database/migrations/1_initial.go @@ -1,9 +1,10 @@ -package migrate +package migrations import ( + "context" "fmt" - "github.com/go-pg/migrations" + "github.com/uptrace/bun" ) const accountTable = ` @@ -43,7 +44,7 @@ func init() { `DROP TABLE accounts`, } - migrations.Register(func(db migrations.DB) error { + Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { fmt.Println("creating initial tables") for _, q := range up { _, err := db.Exec(q) @@ -52,7 +53,7 @@ func init() { } } return nil - }, func(db migrations.DB) error { + }, func(ctx context.Context, db *bun.DB) error { fmt.Println("dropping initial tables") for _, q := range down { _, err := db.Exec(q) diff --git a/database/migrations/2_bootstrap_users.tx.down.sql b/database/migrations/2_bootstrap_users.tx.down.sql new file mode 100644 index 0000000..d5cd279 --- /dev/null +++ b/database/migrations/2_bootstrap_users.tx.down.sql @@ -0,0 +1 @@ +TRUNCATE accounts CASCADE diff --git a/database/migrations/2_bootstrap_users.up.sql b/database/migrations/2_bootstrap_users.up.sql new file mode 100644 index 0000000..f4ab5b1 --- /dev/null +++ b/database/migrations/2_bootstrap_users.up.sql @@ -0,0 +1,8 @@ +INSERT INTO accounts (id, email, name, active, roles) +VALUES (DEFAULT, 'admin@example.com', 'Admin Example', true, '{admin}'); + +--bun:split + +INSERT INTO accounts (id, email, name, active) +VALUES (DEFAULT, 'user@example.com', 'User Example', true); + diff --git a/database/migrate/3_add_profile_table.go b/database/migrations/3_add_profile_table.go similarity index 82% rename from database/migrate/3_add_profile_table.go rename to database/migrations/3_add_profile_table.go index c4c78c1..5c0b554 100644 --- a/database/migrate/3_add_profile_table.go +++ b/database/migrations/3_add_profile_table.go @@ -1,9 +1,10 @@ -package migrate +package migrations import ( + "context" "fmt" - "github.com/go-pg/migrations" + "github.com/uptrace/bun" ) const profileTable = ` @@ -30,7 +31,7 @@ func init() { `DROP TABLE profiles`, } - migrations.Register(func(db migrations.DB) error { + Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { fmt.Println("create profile table") for _, q := range up { _, err := db.Exec(q) @@ -39,7 +40,7 @@ func init() { } } return nil - }, func(db migrations.DB) error { + }, func(ctx context.Context, db *bun.DB) error { fmt.Println("drop profile table") for _, q := range down { _, err := db.Exec(q) diff --git a/database/migrations/main.go b/database/migrations/main.go new file mode 100644 index 0000000..567ca48 --- /dev/null +++ b/database/migrations/main.go @@ -0,0 +1,49 @@ +package migrations + +import ( + "context" + "embed" + "fmt" + "log" + + "github.com/dhax/go-base/database" + "github.com/uptrace/bun/migrate" +) + +//go:embed *.sql +var sqlMigrations embed.FS + +var Migrations = migrate.NewMigrations() + +func init() { + if err := Migrations.Discover(sqlMigrations); err != nil { + panic(err) + } +} + +// Migrate runs all migrations +func Migrate() { + db, err := database.DBConn() + if err != nil { + log.Fatal(err) + } + defer db.Close() + + migrator := migrate.NewMigrator(db, Migrations) + + err = migrator.Init(context.Background()) + if err != nil { + log.Fatal(err) + } + + group, err := migrator.Migrate(context.Background()) + if err != nil { + log.Fatal(err) + } + + if group.ID == 0 { + fmt.Printf("there are no new migrations to run\n") + } else { + fmt.Printf("migrated to %s\n", group) + } +} diff --git a/database/postgres.go b/database/postgres.go index 12aa4a6..dc6be06 100644 --- a/database/postgres.go +++ b/database/postgres.go @@ -2,54 +2,42 @@ package database import ( - "log" + "context" + "database/sql" "github.com/spf13/viper" - - "github.com/go-pg/pg" + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect/pgdialect" + "github.com/uptrace/bun/driver/pgdriver" + "github.com/uptrace/bun/extra/bundebug" ) // DBConn returns a postgres connection pool. -func DBConn() (*pg.DB, error) { +func DBConn() (*bun.DB, error) { viper.SetDefault("db_network", "tcp") viper.SetDefault("db_addr", "localhost:5432") viper.SetDefault("db_user", "postgres") viper.SetDefault("db_password", "postgres") viper.SetDefault("db_database", "postgres") - db := pg.Connect(&pg.Options{ - Network: viper.GetString("db_network"), - Addr: viper.GetString("db_addr"), - User: viper.GetString("db_user"), - Password: viper.GetString("db_password"), - Database: viper.GetString("db_database"), - }) + dsn := "postgres://" + viper.GetString("db_user") + ":" + viper.GetString("db_password") + "@" + viper.GetString("db_addr") + "/" + viper.GetString("db_database") + "?sslmode=disable" + + sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn))) + + db := bun.NewDB(sqldb, pgdialect.New()) if err := checkConn(db); err != nil { return nil, err } if viper.GetBool("db_debug") { - db.AddQueryHook(&logSQL{}) + db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true))) } return db, nil } -type logSQL struct{} - -func (l *logSQL) BeforeQuery(e *pg.QueryEvent) {} - -func (l *logSQL) AfterQuery(e *pg.QueryEvent) { - query, err := e.FormattedQuery() - if err != nil { - panic(err) - } - log.Println(query) -} - -func checkConn(db *pg.DB) error { +func checkConn(db *bun.DB) error { var n int - _, err := db.QueryOne(pg.Scan(&n), "SELECT 1") - return err + return db.NewSelect().ColumnExpr("1").Scan(context.Background(), &n) } diff --git a/database/profileStore.go b/database/profileStore.go index 004a2d5..a3dcd5a 100644 --- a/database/profileStore.go +++ b/database/profileStore.go @@ -1,17 +1,20 @@ package database import ( + "context" + "database/sql" + "github.com/dhax/go-base/models" - "github.com/go-pg/pg" + "github.com/uptrace/bun" ) // ProfileStore implements database operations for profile management. type ProfileStore struct { - db *pg.DB + db *bun.DB } // NewProfileStore returns a ProfileStore implementation. -func NewProfileStore(db *pg.DB) *ProfileStore { +func NewProfileStore(db *bun.DB) *ProfileStore { return &ProfileStore{ db: db, } @@ -19,16 +22,26 @@ func NewProfileStore(db *pg.DB) *ProfileStore { // Get gets an profile by account ID. func (s *ProfileStore) Get(accountID int) (*models.Profile, error) { - p := models.Profile{AccountID: accountID} - _, err := s.db.Model(&p). + p := &models.Profile{AccountID: accountID} + err := s.db.NewSelect(). + Model(p). Where("account_id = ?", accountID). - SelectOrInsert() + Scan(context.Background()) - return &p, err + if err == sql.ErrNoRows { + _, err = s.db.NewInsert(). + Model(p). + Exec(context.Background()) + } + + return p, err } // Update updates profile. func (s *ProfileStore) Update(p *models.Profile) error { - err := s.db.Update(p) + _, err := s.db.NewUpdate(). + Model(p). + WherePK(). + Exec(context.Background()) return err } diff --git a/docker-compose.yml b/docker-compose.yml index 6a86303..4b68232 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,3 @@ -version: "3.8" - -volumes: - postgres: - services: server: build: @@ -40,3 +35,6 @@ services: - postgres:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: postgres + +volumes: + postgres: diff --git a/go.mod b/go.mod index 23c2506..49526fb 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/dhax/go-base -go 1.18 +go 1.22 + +toolchain go1.23.0 require ( github.com/go-chi/chi/v5 v5.0.10 @@ -10,8 +12,6 @@ require ( github.com/go-chi/render v1.0.3 github.com/go-mail/mail v2.3.1+incompatible github.com/go-ozzo/ozzo-validation v3.6.0+incompatible - github.com/go-pg/migrations v6.7.3+incompatible - github.com/go-pg/pg v8.0.7+incompatible github.com/gofrs/uuid v4.4.0+incompatible github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 github.com/lestrrat-go/jwx/v2 v2.0.13 @@ -20,6 +20,10 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.16.0 + github.com/uptrace/bun v1.2.3 + github.com/uptrace/bun/dialect/pgdialect v1.2.3 + github.com/uptrace/bun/driver/pgdriver v1.2.3 + github.com/uptrace/bun/extra/bundebug v1.2.3 github.com/vanng822/go-premailer v1.20.2 ) @@ -29,6 +33,7 @@ require ( github.com/andybalholm/cascadia v1.1.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/fatih/color v1.17.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gorilla/css v1.0.0 // indirect @@ -41,12 +46,13 @@ require ( github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/onsi/ginkgo v1.16.5 // indirect - github.com/onsi/gomega v1.28.0 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect @@ -54,11 +60,14 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/subosito/gotenv v1.4.2 // indirect + github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect github.com/vanng822/css v1.0.1 // indirect - golang.org/x/crypto v0.13.0 // indirect - golang.org/x/net v0.14.0 // indirect - golang.org/x/sys v0.12.0 // indirect - golang.org/x/text v0.13.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/mail.v2 v2.3.1 // indirect diff --git a/go.sum b/go.sum index 86775dd..0d7e092 100644 --- a/go.sum +++ b/go.sum @@ -68,9 +68,10 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= @@ -92,11 +93,6 @@ github.com/go-mail/mail v2.3.1+incompatible h1:UzNOn0k5lpfVtO31cK3hn6I4VEVGhe3lX github.com/go-mail/mail v2.3.1+incompatible/go.mod h1:VPWjmmNyRsWXQZHVHT3g0YbIINUkSmuKOiLIDkWbL6M= github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE= github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= -github.com/go-pg/migrations v6.7.3+incompatible h1:mKayeWTNGhYA9P9wzZNSDoumJRhfB4fEmfAlxNTVwtA= -github.com/go-pg/migrations v6.7.3+incompatible/go.mod h1:DtFiob3rFxsj0He8fye6Ta4eukFW80IfdY10zb2yH1c= -github.com/go-pg/pg v8.0.7+incompatible h1:ty/sXL1OZLo+47KK9N8llRcmbA9tZasqbQ/OO4ld53g= -github.com/go-pg/pg v8.0.7+incompatible/go.mod h1:a2oXow+aFOrvwcKs3eIA0lNFmMilrxK2sOkB5NWe0vA= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= @@ -138,6 +134,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -162,7 +159,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -177,9 +173,11 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= @@ -195,6 +193,11 @@ github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNB github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -203,19 +206,10 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mssola/user_agent v0.6.0 h1:uwPR4rtWlCHRFyyP9u2KOV0u8iQXmS7Z7feTrstQwk4= github.com/mssola/user_agent v0.6.0/go.mod h1:TTPno8LPY3wAIEKRpAtkdMT0f8SE24pLRGPahjCH4uw= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.28.0 h1:i2rg/p9n/UqIDAMFUJ6qIUUMcsqOuUHgbpbu235Vr1c= -github.com/onsi/gomega v1.28.0/go.mod h1:A1H2JE76sI14WIP57LMKj7FVfCHx3g3BcZVjJG8bjX8= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -223,8 +217,11 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= +github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= @@ -259,12 +256,26 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= github.com/unrolled/render v1.0.3/go.mod h1:gN9T0NhL4Bfbwu8ann7Ry/TGHYfosul+J0obPf6NBdM= +github.com/uptrace/bun v1.2.3 h1:6KDc6YiNlXde38j9ATKufb8o7MS8zllhAOeIyELKrk0= +github.com/uptrace/bun v1.2.3/go.mod h1:8frYFHrO/Zol3I4FEjoXam0HoNk+t5k7aJRl3FXp0mk= +github.com/uptrace/bun/dialect/pgdialect v1.2.3 h1:YyCxxqeL0lgFWRZzKCOt6mnxUsjqITcxSo0mLqgwMUA= +github.com/uptrace/bun/dialect/pgdialect v1.2.3/go.mod h1:Vx9TscyEq1iN4tnirn6yYGwEflz0KG3rBZTBCLpKAjc= +github.com/uptrace/bun/driver/pgdriver v1.2.3 h1:VA5TKB0XW7EtreQq2R8Qu/vCAUX2ECaprxGKI9iDuDE= +github.com/uptrace/bun/driver/pgdriver v1.2.3/go.mod h1:yDiYTZYd4FfXFtV01m4I/RkI33IGj9N254jLStaeJLs= +github.com/uptrace/bun/extra/bundebug v1.2.3 h1:2QBykz9/u4SkN9dnraImDcbrMk2fUhuq2gL6hkh9qSc= +github.com/uptrace/bun/extra/bundebug v1.2.3/go.mod h1:bihsYJxXxWZXwc1R3qALTHvp+npE0ElgaCvcjzyPPdw= github.com/vanng822/css v1.0.1 h1:10yiXc4e8NI8ldU6mSrWmSWMuyWgPr9DZ63RSlsgDw8= github.com/vanng822/css v1.0.1/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w= github.com/vanng822/go-premailer v1.20.2 h1:vKs4VdtfXDqL7IXC2pkiBObc1bXM9bYH3Wa+wYw2DnI= github.com/vanng822/go-premailer v1.20.2/go.mod h1:RAxbRFp6M/B171gsKu8dsyq+Y5NGsUUvYfg+WQWusbE= github.com/vanng822/r2router v0.0.0-20150523112421-1023140a4f30/go.mod h1:1BVq8p2jVr55Ost2PkZWDrG86PiJ/0lxqcXoAcGxvWU= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -284,8 +295,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -324,7 +336,6 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -345,7 +356,6 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -360,8 +370,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -384,7 +394,6 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -393,10 +402,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -417,7 +423,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -427,11 +432,14 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -447,8 +455,9 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -496,7 +505,6 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= @@ -597,19 +605,15 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/models/profile.go b/models/profile.go index 9f46232..7df334c 100644 --- a/models/profile.go +++ b/models/profile.go @@ -4,35 +4,34 @@ package models import ( "time" - "github.com/go-ozzo/ozzo-validation" + validation "github.com/go-ozzo/ozzo-validation" - "github.com/go-pg/pg/orm" + "github.com/uptrace/bun" ) // Profile holds specific application settings linked to an Account. type Profile struct { - ID int `json:"-"` - AccountID int `json:"-"` - UpdatedAt time.Time `json:"updated_at,omitempty"` + ID int `bun:"id,pk,autoincrement" json:"-"` + AccountID int `bun:"account_id,notnull" json:"-"` + UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp" json:"updated_at,omitempty"` - Theme string `json:"theme,omitempty"` + Theme string `bun:"theme" json:"theme,omitempty"` } // BeforeInsert hook executed before database insert operation. -func (p *Profile) BeforeInsert(db orm.DB) error { +func (p *Profile) BeforeInsert(db *bun.DB) error { p.UpdatedAt = time.Now() return nil } // BeforeUpdate hook executed before database update operation. -func (p *Profile) BeforeUpdate(db orm.DB) error { +func (p *Profile) BeforeUpdate(db *bun.DB) error { p.UpdatedAt = time.Now() return p.Validate() } // Validate validates Profile struct and returns validation errors. func (p *Profile) Validate() error { - return validation.ValidateStruct(p, validation.Field(&p.Theme, validation.Required, validation.In("default", "dark")), )