Skip to content

Commit 3df956c

Browse files
lzapezr-ondrej
authored andcommitted
Accounts table, DAO sqlx implementation, seeding
1 parent c6f31b4 commit 3df956c

30 files changed

+552
-87
lines changed

.editorconfig

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
; https://editorconfig.org/
2+
3+
root = true
4+
5+
[*]
6+
insert_final_newline = true
7+
charset = utf-8
8+
trim_trailing_whitespace = true
9+
indent_style = space
10+
indent_size = 2
11+
12+
[{Makefile,go.mod,go.sum,*.go,.gitmodules}]
13+
indent_style = tab
14+
indent_size = 4
15+
16+
[*.md]
17+
indent_size = 4
18+
trim_trailing_whitespace = false
19+
20+
eclint_indent_style = unset
21+
22+
[Dockerfile]
23+
indent_size = 4

.github/workflows/gotest.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ jobs:
99
name: go fmt
1010
runs-on: ubuntu-latest
1111
steps:
12-
- uses: actions/checkout@v2
13-
- uses: actions/setup-go@v2
12+
- uses: actions/checkout@v3
13+
- uses: actions/setup-go@v3
1414
with:
1515
go-version: "1.16"
1616
- uses: Jerome1337/[email protected]
@@ -19,8 +19,8 @@ jobs:
1919
name: go vet
2020
runs-on: ubuntu-latest
2121
steps:
22-
- uses: actions/checkout@v2
23-
- uses: actions/setup-go@v2
22+
- uses: actions/checkout@v3
23+
- uses: actions/setup-go@v3
2424
with:
2525
go-version: "1.16"
2626
- run: |
@@ -30,14 +30,14 @@ jobs:
3030
name: golangci-lint
3131
runs-on: ubuntu-latest
3232
steps:
33-
- uses: actions/checkout@v2
34-
- uses: actions/setup-go@v2
33+
- uses: actions/checkout@v3
34+
- uses: actions/setup-go@v3
3535
with:
3636
go-version: "1.16"
3737
- name: golangci-lint
3838
uses: golangci/golangci-lint-action@v2
3939
with:
40-
version: latest
40+
version: v1.45.2
4141
only-new-issues: true
4242
skip-go-installation: true
4343

@@ -50,4 +50,4 @@ jobs:
5050
with:
5151
go-version: "1.16"
5252
- run: |
53-
go test ./...
53+
go test ./...

.golangci.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,18 @@ linters:
1111
- sql
1212
- module
1313
- unused
14+
disable:
15+
- maligned # deprecated by fieldalignment
16+
- scopelint # deprecated by exportloopref
1417
issues:
1518
exclude-rules:
19+
# If a Stmt is prepared on a DB, it will remain usable for the lifetime of the DB. When the Stmt needs to execute
20+
# on a new underlying connection, it will prepare itself on the new connection automatically.
21+
- path: 'internal/dao/sqlx/(.+)\.go'
22+
linters:
23+
- sqlclosecheck
24+
25+
# easier to write test code
1626
- path: '(.+)_test\.go'
1727
linters:
18-
- forcetypeassert
28+
- forcetypeassert

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ COPY --from=build /build/pbmigrate /pbmigrate
1111
USER 1001
1212
EXPOSE 8000
1313
EXPOSE 5000
14-
ENTRYPOINT ["/pbapi"]
14+
ENTRYPOINT ["/pbapi"]

README.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ Migrations can be found in `internal/db/migrations` in SQL format, the only supp
5252
make generate-migration MIGRATION_NAME=add_new_column
5353
```
5454

55-
To execute migration either use [migrate](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate) command line utility from the golang-migrate library which supports more advanced operations like down, or run `pbmigrate` to perform up migration the same way as the application does when it boots up.
55+
We currently do not allow down migrations, so delete the `down.sql` file and do not commit it into git (it will fail build).
56+
57+
To apply migrations, build and run `pbmigrate` binary. This is exactly what application performs during startup. The `pbmigrate` utility also supports seeding initial data. There are files available in `internal/db/seeds` with various seed configurations. Feel free to create your own configuration which you may or may not want to commit into git. When you set `DB_SEED_SCRIPT` configuration variable, the migration tool will execute all statements from that file. By default, the variable is empty, meaning no data will be seeded.
5658

5759
## Building container
5860

@@ -73,16 +75,22 @@ Here are few points before you start contributing:
7375
* All configuration is done via environment variables.
7476
* Database database models (structs) do belong into `internal/models`.
7577
* Do not put any code logic into database models.
76-
* All database operations (CRUD, eager loading) lives in `internal/dao` (Data Access Objects).
78+
* All database operations (CRUD, eager loading) lives in `internal/dao` (Data Access Objects) with a common API interface.
79+
* The actual implementation lives in `internal/dao/sqlx` package, all operations are passed with Context and errors are wrapped.
7780
* Database models must be not exposed directly into JSON API, use `internal/payloads` package to wrap them.
7881
* Business logic (the actual code) does belong into `internal/services` package, each API call should have a dedicated file.
7982
* HTTP routes go into `internal/routes` package.
8083
* HTTP middleware go into `internal/middleware` package.
81-
* Monitoring metrics are in `internal/metrics' package.`
82-
* Use the standard library context package for context operations. Context keys must be defined in `internal/ctxval`.
83-
* Database connection is at `internal/db`, cloud connections are in 'internal/clouds'.
84-
* Do not introduce `utils` or `tools` common packages, they tend to grow.
84+
* Monitoring metrics are in `internal/metrics` package.
85+
* Use the standard library context package for context operations. Context keys are defined in `internal/ctxval` as well as accessor functions.
86+
* Database connection is at `internal/db`, cloud connections are in `internal/clouds`.
87+
* Do not introduce `utils` or `tools` common packages.
8588
* Keep the line of sight (happy code path).
89+
* PostgreSQL version we currently use in production is v14+ so take advantage of all modern SQL features.
90+
* Use `BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY` for primary keys.
91+
* Use `TEXT` for string columns, do not apply any limits in the DB: https://wiki.postgresql.org/wiki/Don%27t_Do_This#Text_storage
92+
93+
Keep security in mind: https://github.com/RedHatInsights/secure-coding-checklist
8694

8795
## License
8896

build/ci/.keep

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

build/package/.keep

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

cmd/pbapi/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package main
22

33
import (
4+
// Performs initialization of DAO implementation, must be initialized before any database packages.
5+
_ "github.com/RHEnVision/provisioning-backend/internal/dao/sqlx"
6+
47
"context"
58
"errors"
69
"fmt"

internal/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ var config struct {
1717
Name string
1818
User string
1919
Password string
20+
SeedScript string
2021
MaxIdleTime time.Duration
2122
MaxLifetime time.Duration
2223
MaxOpenConn int

internal/ctxval/ctx_getters.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package ctxval
2+
3+
import (
4+
"context"
5+
"github.com/rs/zerolog"
6+
)
7+
8+
func GetStringValue(ctx context.Context, key CommonKeyId) string {
9+
return ctx.Value(key).(string)
10+
}
11+
12+
func GetUInt64Value(ctx context.Context, key CommonKeyId) uint64 {
13+
return ctx.Value(key).(uint64)
14+
}
15+
16+
// GetLogger returns logger or nil when not in the context
17+
func GetLogger(ctx context.Context) *zerolog.Logger {
18+
if ctx.Value(LoggerCtxKey) == nil {
19+
return nil
20+
}
21+
logger := ctx.Value(LoggerCtxKey).(zerolog.Logger)
22+
return &logger
23+
}
24+
25+
// GetRequestId returns request id or an empty string when not in the context
26+
func GetRequestId(ctx context.Context) string {
27+
if ctx.Value(RequestIdCtxKey) == nil {
28+
return ""
29+
}
30+
return ctx.Value(RequestIdCtxKey).(string)
31+
}

internal/ctxval/ctx_keys.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package ctxval
2+
3+
type CommonKeyId int
4+
5+
const (
6+
LoggerCtxKey CommonKeyId = iota
7+
RequestIdCtxKey CommonKeyId = iota
8+
RequestNumCtxKey CommonKeyId = iota
9+
ResourceCtxKey CommonKeyId = iota
10+
)

internal/ctxval/keys.go

Lines changed: 0 additions & 20 deletions
This file was deleted.

internal/dao/dao_errors.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package dao
2+
3+
import (
4+
"context"
5+
"fmt"
6+
)
7+
8+
// Error type for all DAO errors.
9+
type Error struct {
10+
Message string
11+
Context context.Context
12+
Err error
13+
}
14+
15+
func (e *Error) Error() string {
16+
return fmt.Sprintf("DAO error: %s: %s", e.Message, e.Err.Error())
17+
}
18+
19+
func (e *Error) Unwrap() error {
20+
return e.Err
21+
}

internal/dao/dao_interfaces.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package dao
2+
3+
import (
4+
"context"
5+
"github.com/RHEnVision/provisioning-backend/internal/models"
6+
)
7+
8+
var GetAccountDao func(ctx context.Context) (AccountDao, error)
9+
10+
type AccountDao interface {
11+
GetById(ctx context.Context, id uint64) (*models.Account, error)
12+
GetByAccountNumber(ctx context.Context, number string) (*models.Account, error)
13+
GetByOrgId(ctx context.Context, orgId string) (*models.Account, error)
14+
List(ctx context.Context, limit, offset uint64) ([]*models.Account, error)
15+
}

internal/dao/sqlx/account_dao.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package sqlx
2+
3+
import (
4+
"context"
5+
"github.com/RHEnVision/provisioning-backend/internal/dao"
6+
"github.com/RHEnVision/provisioning-backend/internal/db"
7+
"github.com/RHEnVision/provisioning-backend/internal/models"
8+
"github.com/jmoiron/sqlx"
9+
)
10+
11+
const (
12+
getById = `SELECT * FROM accounts WHERE id = $1 LIMIT 1`
13+
getByAccountNumber = `SELECT * FROM accounts WHERE account_number = $1 LIMIT 1`
14+
getByOrgId = `SELECT * FROM accounts WHERE org_id = $1 LIMIT 1`
15+
list = `SELECT * FROM accounts ORDER BY id LIMIT $1 OFFSET $2`
16+
)
17+
18+
type accountDaoSqlx struct {
19+
getById *sqlx.Stmt
20+
getByAccountNumber *sqlx.Stmt
21+
getByOrgId *sqlx.Stmt
22+
list *sqlx.Stmt
23+
}
24+
25+
func getAccountDao(ctx context.Context) (dao.AccountDao, error) {
26+
var err error
27+
daoImpl := accountDaoSqlx{}
28+
29+
daoImpl.getById, err = db.DB.PreparexContext(ctx, getById)
30+
if err != nil {
31+
return nil, NewPrepareStatementError(ctx, getById, err)
32+
}
33+
daoImpl.getByAccountNumber, err = db.DB.PreparexContext(ctx, getByAccountNumber)
34+
if err != nil {
35+
return nil, NewPrepareStatementError(ctx, list, err)
36+
}
37+
daoImpl.getByOrgId, err = db.DB.PreparexContext(ctx, getByOrgId)
38+
if err != nil {
39+
return nil, NewPrepareStatementError(ctx, list, err)
40+
}
41+
daoImpl.list, err = db.DB.PreparexContext(ctx, list)
42+
if err != nil {
43+
return nil, NewPrepareStatementError(ctx, list, err)
44+
}
45+
46+
return &daoImpl, nil
47+
}
48+
49+
func init() {
50+
dao.GetAccountDao = getAccountDao
51+
}
52+
53+
func (a *accountDaoSqlx) GetById(ctx context.Context, id uint64) (*models.Account, error) {
54+
result := &models.Account{}
55+
err := a.getById.GetContext(ctx, result, id)
56+
if err != nil {
57+
return nil, NewGetError(ctx, "get by id", err)
58+
}
59+
return result, nil
60+
}
61+
62+
func (a *accountDaoSqlx) GetByAccountNumber(ctx context.Context, number string) (*models.Account, error) {
63+
result := &models.Account{}
64+
err := a.getByAccountNumber.GetContext(ctx, result, number)
65+
if err != nil {
66+
return nil, NewGetError(ctx, "get by id", err)
67+
}
68+
return result, nil
69+
}
70+
71+
func (a *accountDaoSqlx) GetByOrgId(ctx context.Context, orgId string) (*models.Account, error) {
72+
result := &models.Account{}
73+
err := a.getByOrgId.GetContext(ctx, result, orgId)
74+
if err != nil {
75+
return nil, NewGetError(ctx, "get by id", err)
76+
}
77+
return result, nil
78+
}
79+
80+
func (a *accountDaoSqlx) List(ctx context.Context, limit, offset uint64) ([]*models.Account, error) {
81+
var result []*models.Account
82+
err := a.list.SelectContext(ctx, &result, limit, offset)
83+
if err != nil {
84+
return nil, NewGetError(ctx, "list", err)
85+
}
86+
return result, nil
87+
}

internal/dao/sqlx/sqlx_errors.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package sqlx
2+
3+
import (
4+
"context"
5+
"github.com/RHEnVision/provisioning-backend/internal/ctxval"
6+
"github.com/RHEnVision/provisioning-backend/internal/dao"
7+
)
8+
9+
func NewPrepareStatementError(context context.Context, sql string, err error) *dao.Error {
10+
if logger := ctxval.GetLogger(context); logger != nil {
11+
logger.Error().Msgf("sqlx prepare statement error: %s: %v", sql, err)
12+
}
13+
return &dao.Error{
14+
Message: sql,
15+
Context: context,
16+
Err: err,
17+
}
18+
}
19+
20+
func NewGetError(context context.Context, msg string, err error) *dao.Error {
21+
if logger := ctxval.GetLogger(context); logger != nil {
22+
logger.Error().Msgf("sqlx get error: %s: %v", msg, err)
23+
}
24+
return &dao.Error{
25+
Message: msg,
26+
Context: context,
27+
Err: err,
28+
}
29+
}

0 commit comments

Comments
 (0)