Skip to content

Commit 5eac14e

Browse files
thomaspoignantThomas POIGNANT
and
Thomas POIGNANT
authored
feat: Refactor Error Handling (#6)
* Rename function * WIP * WIP --------- Co-authored-by: Thomas POIGNANT <thomas.poignant@MacBook-Pro-de-Thomas.local>
1 parent b128e16 commit 5eac14e

File tree

8 files changed

+199
-108
lines changed

8 files changed

+199
-108
lines changed

api/server.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
)
1010

1111
// New creates a new instance of the API server
12-
func New(serverAddress string, flagHandlers handler.Flags, healthHandlers handler.Health) *Server {
12+
func New(serverAddress string, flagHandlers handler.FlagAPIHandler, healthHandlers handler.Health) *Server {
1313
return &Server{
1414
flagHandlers: flagHandlers,
1515
healthHandlers: healthHandlers,
@@ -20,7 +20,7 @@ func New(serverAddress string, flagHandlers handler.Flags, healthHandlers handle
2020

2121
// Server is the struct that represents the API server
2222
type Server struct {
23-
flagHandlers handler.Flags
23+
flagHandlers handler.FlagAPIHandler
2424
healthHandlers handler.Health
2525
apiEcho *echo.Echo
2626
serverAddress string

dao/err/dao_error.go

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package daoErr
2+
3+
type DaoErrorCode string
4+
5+
const (
6+
NotFound DaoErrorCode = "NOT_FOUND"
7+
InvalidUUID DaoErrorCode = "INVALID_UUID"
8+
ConversionError DaoErrorCode = "CONVERSION_ERROR"
9+
DefaultRuleRequired DaoErrorCode = "DEFAULT_RULE_REQUIRED"
10+
UnknownError DaoErrorCode = "UNKNOWN_ERROR"
11+
DatabaseNotInitialized DaoErrorCode = "DATABASE_NOT_INITIALIZED"
12+
)
13+
14+
type DaoError interface {
15+
error
16+
Code() DaoErrorCode
17+
}
18+
19+
func NewDaoError(code DaoErrorCode, err error) DaoError {
20+
return daoError{
21+
error: err,
22+
code: code,
23+
}
24+
}
25+
26+
type daoError struct {
27+
error
28+
code DaoErrorCode
29+
}
30+
31+
func (d daoError) Code() DaoErrorCode {
32+
return d.code
33+
}

dao/err/postgres_error_wrapper.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package daoErr
2+
3+
import (
4+
"database/sql"
5+
"errors"
6+
"github.com/lib/pq"
7+
)
8+
9+
// WrapPostgresError wraps a postgres error into a DaoError to have a DB agnostic error handling in the handlers
10+
func WrapPostgresError(err error) DaoError {
11+
if errors.Is(err, sql.ErrNoRows) {
12+
return NewDaoError(NotFound, err)
13+
}
14+
var pqErr *pq.Error
15+
if errors.As(err, &pqErr) && pqErr.Code == "22P02" {
16+
return NewDaoError(InvalidUUID, err)
17+
}
18+
return NewDaoError(UnknownError, err)
19+
}

dao/flags.go

+8-8
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,29 @@ package dao
22

33
import (
44
"context"
5-
5+
daoErr "github.com/go-feature-flag/app-api/dao/err"
66
"github.com/go-feature-flag/app-api/model"
77
)
88

99
type Flags interface {
1010
// GetFlags return all the flags
11-
GetFlags(ctx context.Context) ([]model.FeatureFlag, error)
11+
GetFlags(ctx context.Context) ([]model.FeatureFlag, daoErr.DaoError)
1212

1313
// GetFlagByID return a flag by its ID
14-
GetFlagByID(ctx context.Context, id string) (model.FeatureFlag, error)
14+
GetFlagByID(ctx context.Context, id string) (model.FeatureFlag, daoErr.DaoError)
1515

1616
// GetFlagByName return a flag by its name
17-
GetFlagByName(ctx context.Context, name string) (model.FeatureFlag, error)
17+
GetFlagByName(ctx context.Context, name string) (model.FeatureFlag, daoErr.DaoError)
1818

1919
// CreateFlag create a new flag, return the id of the flag
20-
CreateFlag(ctx context.Context, flag model.FeatureFlag) (string, error)
20+
CreateFlag(ctx context.Context, flag model.FeatureFlag) (string, daoErr.DaoError)
2121

2222
// UpdateFlag update a flag
23-
UpdateFlag(ctx context.Context, flag model.FeatureFlag) error
23+
UpdateFlag(ctx context.Context, flag model.FeatureFlag) daoErr.DaoError
2424

2525
// DeleteFlagByID delete a flag
26-
DeleteFlagByID(ctx context.Context, id string) error
26+
DeleteFlagByID(ctx context.Context, id string) daoErr.DaoError
2727

2828
// Ping check that the data layer is available
29-
Ping() error
29+
Ping() daoErr.DaoError
3030
}

dao/postgres_impl.go

+59-45
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import (
44
"context"
55
"errors"
66
"fmt"
7-
87
"github.com/go-feature-flag/app-api/dao/dbmodel"
8+
daoErr "github.com/go-feature-flag/app-api/dao/err"
99
"github.com/go-feature-flag/app-api/model"
1010
"github.com/google/uuid"
1111
"github.com/jackc/pgx/v5"
@@ -35,38 +35,38 @@ type pgFlagImpl struct {
3535
}
3636

3737
// GetFlags return all the flags
38-
func (m *pgFlagImpl) GetFlags(ctx context.Context) ([]model.FeatureFlag, error) {
38+
func (m *pgFlagImpl) GetFlags(ctx context.Context) ([]model.FeatureFlag, daoErr.DaoError) {
3939
var f []dbmodel.FeatureFlag
40-
err := m.conn.SelectContext(ctx, &f, "SELECT * FROM feature_flags")
40+
err := m.conn.SelectContext(ctx, &f, "SELECT * FROM feature_flags ORDER BY last_updated_date DESC")
4141
if err != nil {
4242
if errors.Is(err, pgx.ErrNoRows) {
4343
return []model.FeatureFlag{}, nil
4444
}
45-
return []model.FeatureFlag{}, err
45+
return []model.FeatureFlag{}, daoErr.WrapPostgresError(err)
4646
}
4747
res := make([]model.FeatureFlag, 0, len(f))
4848
for _, flag := range f {
4949
var rules []dbmodel.Rule
5050
err := m.conn.SelectContext(ctx, &rules, `SELECT * FROM rules WHERE feature_flag_id = $1`, flag.ID)
5151
if err != nil {
52-
return []model.FeatureFlag{}, err
52+
return []model.FeatureFlag{}, daoErr.WrapPostgresError(err)
5353
}
5454

5555
convertedFlag, err := flag.ToModelFeatureFlag(rules)
5656
if err != nil {
57-
return []model.FeatureFlag{}, err
57+
return []model.FeatureFlag{}, daoErr.WrapPostgresError(err)
5858
}
5959
res = append(res, convertedFlag)
6060
}
6161
return res, nil
6262
}
6363

6464
// GetFlagByID return a flag by its ID
65-
func (m *pgFlagImpl) GetFlagByID(ctx context.Context, id string) (model.FeatureFlag, error) {
65+
func (m *pgFlagImpl) GetFlagByID(ctx context.Context, id string) (model.FeatureFlag, daoErr.DaoError) {
6666
var f dbmodel.FeatureFlag
6767
err := m.conn.GetContext(ctx, &f, `SELECT * FROM feature_flags WHERE id = $1`, id)
6868
if err != nil {
69-
return model.FeatureFlag{}, err
69+
return model.FeatureFlag{}, daoErr.WrapPostgresError(err)
7070
}
7171

7272
var rules []dbmodel.Rule
@@ -76,48 +76,48 @@ func (m *pgFlagImpl) GetFlagByID(ctx context.Context, id string) (model.FeatureF
7676
`SELECT * FROM rules WHERE feature_flag_id = $1 ORDER BY order_index`, f.ID)
7777

7878
if errRule != nil {
79-
return model.FeatureFlag{}, errRule
79+
return model.FeatureFlag{}, daoErr.WrapPostgresError(errRule)
8080
}
8181

8282
convertedFlag, err := f.ToModelFeatureFlag(rules)
8383
if err != nil {
84-
return model.FeatureFlag{}, err
84+
return model.FeatureFlag{}, daoErr.NewDaoError(daoErr.ConversionError, err)
8585
}
8686
return convertedFlag, nil
8787
}
8888

8989
// GetFlagByName return a flag by its name
90-
func (m *pgFlagImpl) GetFlagByName(ctx context.Context, name string) (model.FeatureFlag, error) {
90+
func (m *pgFlagImpl) GetFlagByName(ctx context.Context, name string) (model.FeatureFlag, daoErr.DaoError) {
9191
var f dbmodel.FeatureFlag
9292
err := m.conn.GetContext(ctx, &f, `SELECT * FROM feature_flags WHERE name = $1`, name)
9393
if err != nil {
94-
return model.FeatureFlag{}, err
94+
return model.FeatureFlag{}, daoErr.WrapPostgresError(err)
9595
}
9696

9797
var rules []dbmodel.Rule
9898
errRule := m.conn.SelectContext(ctx, &rules,
9999
`SELECT * FROM rules WHERE feature_flag_id = $1 ORDER BY order_index DESC`, f.ID)
100100
if errRule != nil {
101-
return model.FeatureFlag{}, errRule
101+
return model.FeatureFlag{}, daoErr.WrapPostgresError(errRule)
102102
}
103103

104104
convertedFlag, err := f.ToModelFeatureFlag(rules)
105105
if err != nil {
106-
return model.FeatureFlag{}, err
106+
return model.FeatureFlag{}, daoErr.NewDaoError(daoErr.ConversionError, err)
107107
}
108108
return convertedFlag, nil
109109
}
110110

111111
// CreateFlag create a new flag, return the id of the flag
112-
func (m *pgFlagImpl) CreateFlag(ctx context.Context, flag model.FeatureFlag) (string, error) {
112+
func (m *pgFlagImpl) CreateFlag(ctx context.Context, flag model.FeatureFlag) (string, daoErr.DaoError) {
113113
dbFeatureFlag, err := dbmodel.FromModelFeatureFlag(flag)
114114
if err != nil {
115-
return "", err
115+
return "", daoErr.NewDaoError(daoErr.ConversionError, err)
116116
}
117117

118118
tx, err := m.conn.Beginx()
119119
if err != nil {
120-
return "", err
120+
return "", daoErr.WrapPostgresError(err)
121121
}
122122
defer func() { _ = tx.Commit() }()
123123
_, err = tx.NamedExecContext(
@@ -153,64 +153,64 @@ func (m *pgFlagImpl) CreateFlag(ctx context.Context, flag model.FeatureFlag) (st
153153
dbFeatureFlag)
154154
if err != nil {
155155
_ = tx.Rollback()
156-
return "", err
156+
return "", daoErr.WrapPostgresError(err)
157157
}
158158

159159
if flag.DefaultRule == nil {
160-
return "", fmt.Errorf("default rule is required")
160+
return "", daoErr.NewDaoError(daoErr.DefaultRuleRequired, fmt.Errorf("default rule is required"))
161161
}
162162
err = m.insertRule(ctx, *flag.DefaultRule, true, dbFeatureFlag.ID, tx, -1)
163163
if err != nil {
164164
_ = tx.Rollback()
165-
return "", err
165+
return "", daoErr.WrapPostgresError(err)
166166
}
167167

168168
if flag.Rules != nil {
169169
for index, rule := range *flag.Rules {
170170
err = m.insertRule(ctx, rule, false, dbFeatureFlag.ID, tx, index)
171171
if err != nil {
172172
_ = tx.Rollback()
173-
return "", err
173+
return "", daoErr.WrapPostgresError(err)
174174
}
175175
}
176176
}
177177

178178
err = tx.Commit()
179179
if err != nil {
180180
_ = tx.Rollback()
181-
return "", err
181+
return "", daoErr.WrapPostgresError(err)
182182
}
183183
return dbFeatureFlag.ID.String(), nil
184184
}
185185

186-
func (m *pgFlagImpl) UpdateFlag(ctx context.Context, flag model.FeatureFlag) error {
187-
dbQuery, err := dbmodel.FromModelFeatureFlag(flag)
188-
if err != nil {
189-
return err
186+
func (m *pgFlagImpl) UpdateFlag(ctx context.Context, flag model.FeatureFlag) daoErr.DaoError {
187+
dbQuery, errConv := dbmodel.FromModelFeatureFlag(flag)
188+
if errConv != nil {
189+
return daoErr.NewDaoError(daoErr.ConversionError, errConv)
190190
}
191191
tx, err := m.conn.Beginx()
192192
if err != nil {
193-
return err
193+
return daoErr.WrapPostgresError(err)
194194
}
195195

196196
flagOrder := map[string]int{}
197197
for i, rule := range flag.GetRules() {
198198
flagOrder[rule.ID] = i
199199
}
200200

201-
dbFF, err := m.GetFlagByID(ctx, flag.ID)
202-
if err != nil {
203-
return err
201+
dbFF, getFlagErr := m.GetFlagByID(ctx, flag.ID)
202+
if getFlagErr != nil {
203+
return getFlagErr
204204
}
205205

206206
// update default rule
207207
if flag.DefaultRule == nil {
208-
return fmt.Errorf("default rule is required")
208+
return daoErr.NewDaoError(daoErr.DefaultRuleRequired, fmt.Errorf("default rule is required"))
209209
}
210210

211211
if err := m.updateRule(ctx, flag.GetDefaultRule(), true, dbQuery.ID, tx, -1); err != nil {
212212
_ = tx.Rollback
213-
return err
213+
return daoErr.WrapPostgresError(err)
214214
}
215215

216216
listExistingRuleIDs := make(map[string]model.Rule)
@@ -241,23 +241,23 @@ func (m *pgFlagImpl) UpdateFlag(ctx context.Context, flag model.FeatureFlag) err
241241
for _, id := range toDelete {
242242
if _, err := tx.ExecContext(ctx, `DELETE FROM rules WHERE id = $1`, id); err != nil {
243243
_ = tx.Rollback
244-
return err
244+
return daoErr.WrapPostgresError(err)
245245
}
246246
}
247247

248248
for _, id := range toCreate {
249249
rule := listNewRuleIDs[id]
250250
if err := m.insertRule(ctx, rule, false, dbQuery.ID, tx, flagOrder[dbQuery.ID.String()]); err != nil {
251251
_ = tx.Rollback
252-
return err
252+
return daoErr.WrapPostgresError(err)
253253
}
254254
}
255255

256256
for _, id := range toUpdate {
257257
rule := listNewRuleIDs[id]
258258
if err = m.updateRule(ctx, rule, false, dbQuery.ID, tx, flagOrder[dbQuery.ID.String()]); err != nil {
259259
_ = tx.Rollback
260-
return err
260+
return daoErr.WrapPostgresError(err)
261261
}
262262
}
263263

@@ -278,30 +278,40 @@ func (m *pgFlagImpl) UpdateFlag(ctx context.Context, flag model.FeatureFlag) err
278278
WHERE id = :id`,
279279
dbQuery); err != nil {
280280
_ = tx.Rollback
281-
return errTx
281+
return daoErr.WrapPostgresError(errTx)
282282
}
283283

284-
return tx.Commit()
284+
commitErr := tx.Commit()
285+
if commitErr != nil {
286+
_ = tx.Rollback
287+
return daoErr.WrapPostgresError(commitErr)
288+
}
289+
return nil
285290
}
286291

287-
func (m *pgFlagImpl) DeleteFlagByID(ctx context.Context, id string) error {
292+
func (m *pgFlagImpl) DeleteFlagByID(ctx context.Context, id string) daoErr.DaoError {
288293
tx, err := m.conn.Beginx()
289294
if err != nil {
290-
return err
295+
return daoErr.WrapPostgresError(err)
291296
}
292297

293298
_, err = tx.ExecContext(ctx, `DELETE FROM rules WHERE feature_flag_id = $1`, id)
294299
if err != nil {
295300
_ = tx.Rollback()
296-
return err
301+
return daoErr.WrapPostgresError(err)
297302
}
298303

299304
_, err = tx.ExecContext(ctx, `DELETE FROM feature_flags WHERE id = $1`, id)
300305
if err != nil {
301306
_ = tx.Rollback()
302-
return err
307+
return daoErr.WrapPostgresError(err)
303308
}
304-
return tx.Commit()
309+
commitErr := tx.Commit()
310+
if commitErr != nil {
311+
_ = tx.Rollback
312+
return daoErr.WrapPostgresError(commitErr)
313+
}
314+
return nil
305315
}
306316

307317
func (m *pgFlagImpl) insertRule(
@@ -383,9 +393,13 @@ func (m *pgFlagImpl) updateRule(
383393
return errTx
384394
}
385395

386-
func (m *pgFlagImpl) Ping() error {
396+
func (m *pgFlagImpl) Ping() daoErr.DaoError {
387397
if m.conn == nil {
388-
return errors.New("database connection is nil")
398+
return daoErr.NewDaoError(daoErr.DatabaseNotInitialized, errors.New("database connection is nil"))
399+
}
400+
err := m.conn.Ping()
401+
if err != nil {
402+
return daoErr.WrapPostgresError(err)
389403
}
390-
return m.conn.Ping()
404+
return nil
391405
}

0 commit comments

Comments
 (0)