Skip to content

Commit 1886be6

Browse files
committed
upgrade from go-pg to bun
1 parent f59f129 commit 1886be6

23 files changed

+413
-383
lines changed

README.md

+8-7
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The following feature set is a minimal selection of typical Web API requirements
1414

1515
- Configuration using [viper](https://github.com/spf13/viper)
1616
- CLI features using [cobra](https://github.com/spf13/cobra)
17-
- PostgreSQL support including migrations using [go-pg](https://github.com/go-pg/pg)
17+
- PostgreSQL support including migrations using [bun](https://github.com/uptrace/bun)
1818
- Structured logging with [Logrus](https://github.com/sirupsen/logrus)
1919
- Routing with [chi router](https://github.com/go-chi/chi) and middleware
2020
- 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
3535
### Using Docker Compose
3636

3737
- First start the database only: `docker compose up -d postgres`
38-
- Once initialize the database by running all migrations in database/migrate folder: `docker compose exec server ./main migrate`
39-
- Start the api server: `docker compose up server`
38+
- Once initialize the database by running all migrations in database/migrate folder: `docker compose run server ./main migrate`
39+
- Start the api server: `docker compose up`
4040

4141
## API Routes
4242

@@ -53,7 +53,7 @@ For passwordless login following routes are available:
5353

5454
### Example API
5555

56-
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.
56+
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.
5757

5858
Check [routes.md](routes.md) for a generated overview of the provided API routes.
5959

@@ -71,9 +71,10 @@ Outgoing emails containing the login token will be print to stdout if no valid e
7171

7272
Use one of the following bootstrapped users for login:
7373

74-
- admin@boot.io (has access to admin panel)
75-
- user@boot.io
74+
- <admin@example.com> (has access to admin panel)
75+
- <user@example.com>
7676

77+
TODO: deploy somewhere else...
7778
A deployed version can also be found on [Heroku](https://govue.herokuapp.com)
7879

7980
### Testing
@@ -94,7 +95,7 @@ By default viper will look at $HOME/.go-base.yaml for a config file. Setting you
9495
| DB_USER | string | postgres | database user name |
9596
| DB_PASSWORD | string | postgres | database user password |
9697
| DB_DATABASE | string | postgres | database shema name |
97-
| AUTH_LOGIN_URL | string | http://localhost:3000/login | client login url as sent in login token email |
98+
| AUTH_LOGIN_URL | string | <http://localhost:3000/login> | client login url as sent in login token email |
9899
| AUTH_LOGIN_TOKEN_LENGTH | int | 8 | length of login token |
99100
| AUTH_LOGIN_TOKEN_EXPIRY | time.Duration | 11m | login token expiry |
100101
| 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) |

api/admin/api.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import (
55
"net/http"
66

77
"github.com/sirupsen/logrus"
8+
"github.com/uptrace/bun"
89

910
"github.com/go-chi/chi/v5"
10-
"github.com/go-pg/pg"
1111

1212
"github.com/dhax/go-base/auth/authorize"
1313
"github.com/dhax/go-base/database"
@@ -30,8 +30,7 @@ type API struct {
3030
}
3131

3232
// NewAPI configures and returns admin application API.
33-
func NewAPI(db *pg.DB) (*API, error) {
34-
33+
func NewAPI(db *bun.DB) (*API, error) {
3534
accountStore := database.NewAdmAccountStore(db)
3635
accounts := NewAccountResource(accountStore)
3736

api/api.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,7 @@ func New(enableCORS bool) (*chi.Mux, error) {
8282
w.Write([]byte("pong"))
8383
})
8484

85-
client := "./public"
86-
r.Get("/*", SPAHandler(client))
85+
r.Get("/*", SPAHandler("public"))
8786

8887
return r, nil
8988
}

api/app/api.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import (
55
"net/http"
66

77
"github.com/go-chi/chi/v5"
8-
"github.com/go-pg/pg"
98
"github.com/sirupsen/logrus"
9+
"github.com/uptrace/bun"
1010

1111
"github.com/dhax/go-base/database"
1212
"github.com/dhax/go-base/logging"
@@ -26,7 +26,7 @@ type API struct {
2626
}
2727

2828
// NewAPI configures and returns application API.
29-
func NewAPI(db *pg.DB) (*API, error) {
29+
func NewAPI(db *bun.DB) (*API, error) {
3030
accountStore := database.NewAccountStore(db)
3131
account := NewAccountResource(accountStore)
3232

auth/jwt/token.go

+11-11
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,24 @@ package jwt
33
import (
44
"time"
55

6-
"github.com/go-pg/pg/orm"
6+
"github.com/uptrace/bun"
77
)
88

99
// Token holds refresh jwt information.
1010
type Token struct {
11-
ID int `json:"id,omitempty"`
12-
CreatedAt time.Time `json:"created_at,omitempty"`
13-
UpdatedAt time.Time `json:"updated_at,omitempty"`
14-
AccountID int `json:"-"`
11+
ID int `bun:"id,pk,autoincrement" json:"id,omitempty"`
12+
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp" json:"created_at,omitempty"`
13+
UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp" json:"updated_at,omitempty"`
14+
AccountID int `bun:"account_id,notnull" json:"-"`
1515

16-
Token string `json:"-"`
17-
Expiry time.Time `json:"-"`
18-
Mobile bool `sql:",notnull" json:"mobile"`
19-
Identifier string `json:"identifier,omitempty"`
16+
Token string `bun:"token,notnull" json:"-"`
17+
Expiry time.Time `bun:"expiry,notnull" json:"-"`
18+
Mobile bool `bun:"mobile,notnull" json:"mobile"`
19+
Identifier string `bun:"identifier" json:"identifier,omitempty"`
2020
}
2121

2222
// BeforeInsert hook executed before database insert operation.
23-
func (t *Token) BeforeInsert(db orm.DB) error {
23+
func (t *Token) BeforeInsert(db *bun.DB) error {
2424
now := time.Now()
2525
if t.CreatedAt.IsZero() {
2626
t.CreatedAt = now
@@ -30,7 +30,7 @@ func (t *Token) BeforeInsert(db orm.DB) error {
3030
}
3131

3232
// BeforeUpdate hook executed before database update operation.
33-
func (t *Token) BeforeUpdate(db orm.DB) error {
33+
func (t *Token) BeforeUpdate(db *bun.DB) error {
3434
t.UpdatedAt = time.Now()
3535
return nil
3636
}

auth/pwdless/account.go

+13-13
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,28 @@ import (
66

77
validation "github.com/go-ozzo/ozzo-validation"
88
"github.com/go-ozzo/ozzo-validation/is"
9-
"github.com/go-pg/pg/orm"
9+
"github.com/uptrace/bun"
1010

1111
"github.com/dhax/go-base/auth/jwt"
1212
)
1313

1414
// Account represents an authenticated application user
1515
type Account struct {
16-
ID int `json:"id"`
17-
CreatedAt time.Time `json:"created_at,omitempty"`
18-
UpdatedAt time.Time `json:"updated_at,omitempty"`
19-
LastLogin time.Time `json:"last_login,omitempty"`
16+
ID int `bun:"id,pk,autoincrement" json:"id"`
17+
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp" json:"created_at,omitempty"`
18+
UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp" json:"updated_at,omitempty"`
19+
LastLogin time.Time `bun:"last_login" json:"last_login,omitempty"`
2020

21-
Email string `json:"email"`
22-
Name string `json:"name"`
23-
Active bool `sql:",notnull" json:"active"`
24-
Roles []string `pg:",array" json:"roles,omitempty"`
21+
Email string `bun:"email,notnull" json:"email"`
22+
Name string `bun:"name,notnull" json:"name"`
23+
Active bool `bun:"active,notnull" json:"active"`
24+
Roles []string `bun:"roles,array" json:"roles,omitempty"`
2525

26-
Token []jwt.Token `json:"token,omitempty"`
26+
Token []jwt.Token `bun:"rel:has-many" json:"token,omitempty"`
2727
}
2828

2929
// BeforeInsert hook executed before database insert operation.
30-
func (a *Account) BeforeInsert(db orm.DB) error {
30+
func (a *Account) BeforeInsert(db *bun.DB) error {
3131
now := time.Now()
3232
if a.CreatedAt.IsZero() {
3333
a.CreatedAt = now
@@ -37,13 +37,13 @@ func (a *Account) BeforeInsert(db orm.DB) error {
3737
}
3838

3939
// BeforeUpdate hook executed before database update operation.
40-
func (a *Account) BeforeUpdate(db orm.DB) error {
40+
func (a *Account) BeforeUpdate(db *bun.DB) error {
4141
a.UpdatedAt = time.Now()
4242
return a.Validate()
4343
}
4444

4545
// BeforeDelete hook executed before database delete operation.
46-
func (a *Account) BeforeDelete(db orm.DB) error {
46+
func (a *Account) BeforeDelete(db *bun.DB) error {
4747
return nil
4848
}
4949

cmd/migrate.go

+4-19
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,16 @@ package cmd
33
import (
44
"github.com/spf13/cobra"
55

6-
"github.com/dhax/go-base/database/migrate"
6+
"github.com/dhax/go-base/database/migrations"
77
)
88

9-
var reset bool
10-
119
// migrateCmd represents the migrate command
1210
var migrateCmd = &cobra.Command{
1311
Use: "migrate",
14-
Short: "use go-pg migration tool",
15-
Long: `migrate uses go-pg migration tool under the hood supporting the same commands and an additional reset command`,
12+
Short: "use bun migration tool",
13+
Long: `run bun migrations`,
1614
Run: func(cmd *cobra.Command, args []string) {
17-
argsMig := args[:0]
18-
for _, arg := range args {
19-
switch arg {
20-
case "migrate", "--db_debug", "--reset":
21-
default:
22-
argsMig = append(argsMig, arg)
23-
}
24-
}
25-
26-
if reset {
27-
migrate.Reset()
28-
}
29-
migrate.Migrate(argsMig)
15+
migrations.Migrate()
3016
},
3117
}
3218

@@ -42,5 +28,4 @@ func init() {
4228
// Cobra supports local flags which will only run when this command
4329
// is called directly, e.g.:
4430
// migrateCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
45-
migrateCmd.Flags().BoolVar(&reset, "reset", false, "migrate down to version 0 then up to latest. WARNING: all data will be lost!")
4631
}

database/accountStore.go

+50-28
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,94 @@
11
package database
22

33
import (
4+
"context"
5+
"database/sql"
6+
47
"github.com/dhax/go-base/auth/jwt"
58
"github.com/dhax/go-base/auth/pwdless"
69
"github.com/dhax/go-base/models"
7-
"github.com/go-pg/pg"
10+
"github.com/uptrace/bun"
811
)
912

1013
// AccountStore implements database operations for account management by user.
1114
type AccountStore struct {
12-
db *pg.DB
15+
db *bun.DB
1316
}
1417

1518
// NewAccountStore returns an AccountStore.
16-
func NewAccountStore(db *pg.DB) *AccountStore {
19+
func NewAccountStore(db *bun.DB) *AccountStore {
1720
return &AccountStore{
1821
db: db,
1922
}
2023
}
2124

2225
// Get an account by ID.
2326
func (s *AccountStore) Get(id int) (*pwdless.Account, error) {
24-
a := pwdless.Account{ID: id}
25-
err := s.db.Model(&a).
26-
Where("account.id = ?id").
27-
Column("account.*", "Token").
28-
First()
29-
return &a, err
27+
a := &pwdless.Account{ID: id}
28+
err := s.db.NewSelect().
29+
Model(a).
30+
Where("id = ?", id).
31+
Relation("Token").
32+
Scan(context.Background())
33+
return a, err
3034
}
3135

3236
// Update an account.
3337
func (s *AccountStore) Update(a *pwdless.Account) error {
34-
_, err := s.db.Model(a).
38+
_, err := s.db.NewUpdate().
39+
Model(a).
3540
Column("email", "name").
3641
WherePK().
37-
Update()
42+
Exec(context.Background())
3843
return err
3944
}
4045

4146
// Delete an account.
4247
func (s *AccountStore) Delete(a *pwdless.Account) error {
43-
err := s.db.RunInTransaction(func(tx *pg.Tx) error {
44-
if _, err := tx.Model(&jwt.Token{}).
45-
Where("account_id = ?", a.ID).
46-
Delete(); err != nil {
47-
return err
48-
}
49-
if _, err := tx.Model(&models.Profile{}).
50-
Where("account_id = ?", a.ID).
51-
Delete(); err != nil {
52-
return err
53-
}
54-
return tx.Delete(a)
55-
})
56-
return err
48+
ctx := context.Background()
49+
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
50+
if err != nil {
51+
return err
52+
}
53+
if _, err := tx.NewDelete().
54+
Model((*jwt.Token)(nil)).
55+
Where("account_id = ?", a.ID).
56+
Exec(ctx); err != nil {
57+
tx.Rollback()
58+
return err
59+
}
60+
if _, err := tx.NewDelete().
61+
Model((*models.Profile)(nil)).
62+
Where("account_id = ?", a.ID).
63+
Exec(ctx); err != nil {
64+
tx.Rollback()
65+
return err
66+
}
67+
if _, err := tx.NewDelete().
68+
Model(a).
69+
WherePK().
70+
Exec(ctx); err != nil {
71+
tx.Rollback()
72+
}
73+
tx.Commit()
74+
return nil
5775
}
5876

5977
// UpdateToken updates a jwt refresh token.
6078
func (s *AccountStore) UpdateToken(t *jwt.Token) error {
61-
_, err := s.db.Model(t).
79+
_, err := s.db.NewUpdate().
80+
Model(t).
6281
Column("identifier").
6382
WherePK().
64-
Update()
83+
Exec(context.Background())
6584
return err
6685
}
6786

6887
// DeleteToken deletes a jwt refresh token.
6988
func (s *AccountStore) DeleteToken(t *jwt.Token) error {
70-
err := s.db.Delete(t)
89+
_, err := s.db.NewDelete().
90+
Model(t).
91+
WherePK().
92+
Exec(context.Background())
7193
return err
7294
}

0 commit comments

Comments
 (0)