1
1
package handler
2
2
3
3
import (
4
- "database/sql"
5
4
"errors"
6
5
"fmt"
7
- daoErr "github.com/go-feature-flag/app-api/dao/err"
8
- "github.com/labstack/echo/v4"
9
6
"net/http"
10
- "time"
11
7
12
8
"github.com/go-feature-flag/app-api/dao"
9
+ daoErr "github.com/go-feature-flag/app-api/dao/err"
13
10
"github.com/go-feature-flag/app-api/model"
11
+ "github.com/go-feature-flag/app-api/util"
14
12
"github.com/google/uuid"
15
- "github.com/lib/pq "
13
+ "github.com/labstack/echo/v4 "
16
14
)
17
15
16
+ type FlagAPIHandlerOptions struct {
17
+ Clock util.Clock
18
+ }
19
+
18
20
type FlagAPIHandler struct {
19
- dao dao.Flags
21
+ dao dao.Flags
22
+ options * FlagAPIHandlerOptions
20
23
}
21
24
22
25
// NewFlagAPIHandler creates a new instance of the FlagAPIHandler handler
23
26
// It is a controller class to handle the feature flag configuration logic
24
- func NewFlagAPIHandler (dao dao.Flags ) FlagAPIHandler {
25
- return FlagAPIHandler {dao : dao }
27
+ func NewFlagAPIHandler (dao dao.Flags , options * FlagAPIHandlerOptions ) FlagAPIHandler {
28
+ if options == nil {
29
+ options = & FlagAPIHandlerOptions {}
30
+ }
31
+ if options .Clock == nil {
32
+ options .Clock = util.DefaultClock {}
33
+ }
34
+ return FlagAPIHandler {dao : dao , options : options }
26
35
}
27
36
28
37
// GetAllFeatureFlags is returning the list of all the flags
@@ -87,8 +96,8 @@ func (f FlagAPIHandler) CreateNewFlag(c echo.Context) error {
87
96
if flag .ID == "" {
88
97
flag .ID = uuid .NewString ()
89
98
}
90
- flag .CreatedDate = time .Now ()
91
- flag .LastUpdatedDate = time .Now ()
99
+ flag .CreatedDate = f . options . Clock .Now ()
100
+ flag .LastUpdatedDate = f . options . Clock .Now ()
92
101
// TODO: remove this line and extract the information from the token
93
102
flag .LastModifiedBy = "toto"
94
103
@@ -105,6 +114,9 @@ func (f FlagAPIHandler) CreateNewFlag(c echo.Context) error {
105
114
106
115
id , err := f .dao .CreateFlag (c .Request ().Context (), flag )
107
116
if err != nil {
117
+ if err .Code () == daoErr .ConversionError {
118
+ return c .JSON (model .NewHTTPError (http .StatusBadRequest , err ))
119
+ }
108
120
return c .JSON (model .NewHTTPError (http .StatusInternalServerError , err ))
109
121
}
110
122
flag .ID = id
@@ -123,8 +135,17 @@ func validateFlag(flag model.FeatureFlag) (int, error) {
123
135
return status , err
124
136
}
125
137
126
- if flag .VariationType == "" {
138
+ switch flag .VariationType {
139
+ case model .FlagTypeBoolean ,
140
+ model .FlagTypeDouble ,
141
+ model .FlagTypeInteger ,
142
+ model .FlagTypeString ,
143
+ model .FlagTypeJSON :
144
+ break
145
+ case "" :
127
146
return http .StatusBadRequest , errors .New ("flag type is required" )
147
+ default :
148
+ return http .StatusBadRequest , fmt .Errorf ("flag type %s not supported" , flag .VariationType )
128
149
}
129
150
130
151
for _ , rule := range flag .GetRules () {
@@ -137,10 +158,15 @@ func validateFlag(flag model.FeatureFlag) (int, error) {
137
158
}
138
159
139
160
func validateRule (rule * model.Rule , isDefault bool ) (int , error ) {
140
- if rule == nil ||
141
- (rule .ProgressiveRollout == nil &&
142
- rule .Percentages == nil &&
143
- (rule .VariationResult == nil || * rule .VariationResult == "" )) {
161
+ if rule == nil || * rule == (model.Rule {}) {
162
+ if isDefault {
163
+ return http .StatusBadRequest , errors .New ("flag default rule is required" )
164
+ }
165
+ return http .StatusBadRequest , errors .New ("targeting rule is nil" )
166
+ }
167
+ if rule .ProgressiveRollout == nil &&
168
+ rule .Percentages == nil &&
169
+ (rule .VariationResult == nil || * rule .VariationResult == "" ) {
144
170
err := fmt .Errorf ("invalid rule %s" , rule .Name )
145
171
if isDefault {
146
172
err = errors .New ("flag default rule is invalid" )
@@ -150,7 +176,7 @@ func validateRule(rule *model.Rule, isDefault bool) (int, error) {
150
176
151
177
if ! isDefault {
152
178
if rule .Query == "" {
153
- return http .StatusBadRequest , errors .New ("rule query is required" )
179
+ return http .StatusBadRequest , errors .New ("query is required for targeting rules " )
154
180
}
155
181
}
156
182
return http .StatusOK , nil
@@ -168,8 +194,7 @@ func validateRule(rule *model.Rule, isDefault bool) (int, error) {
168
194
// @Failure 500 {object} model.HTTPError "Internal server error"
169
195
// @Router /v1/flags/{id} [put]
170
196
func (f FlagAPIHandler ) UpdateFlagByID (c echo.Context ) error {
171
- // check if the flag exists
172
- _ , err := f .dao .GetFlagByID (c .Request ().Context (), c .Param ("id" ))
197
+ retrievedFlag , err := f .dao .GetFlagByID (c .Request ().Context (), c .Param ("id" ))
173
198
if err != nil {
174
199
return f .handleDaoError (c , err )
175
200
}
@@ -187,11 +212,12 @@ func (f FlagAPIHandler) UpdateFlagByID(c echo.Context) error {
187
212
if flag .ID == "" {
188
213
flag .ID = c .Param ("id" )
189
214
}
190
- flag .LastUpdatedDate = time .Now ()
215
+ flag .LastUpdatedDate = f .options .Clock .Now ()
216
+ flag .CreatedDate = retrievedFlag .CreatedDate
191
217
192
218
err = f .dao .UpdateFlag (c .Request ().Context (), flag )
193
219
if err != nil {
194
- return c . JSON ( model . NewHTTPError ( http . StatusInternalServerError , err ) )
220
+ return f . handleDaoError ( c , err )
195
221
}
196
222
return c .JSON (http .StatusOK , flag )
197
223
}
@@ -210,19 +236,7 @@ func (f FlagAPIHandler) DeleteFlagByID(c echo.Context) error {
210
236
idParam := c .Param ("id" )
211
237
err := f .dao .DeleteFlagByID (c .Request ().Context (), idParam )
212
238
if err != nil {
213
- if errors .Is (err , sql .ErrNoRows ) {
214
- return c .JSON (model .NewHTTPError (http .StatusNotFound , fmt .Errorf ("flag with id %s not found" , idParam )))
215
- }
216
- var pgErr * pq.Error
217
- if errors .As (err , & pgErr ) {
218
- switch pgErr .Code {
219
- case "22P02" :
220
- return c .JSON (model .NewHTTPError (http .StatusBadRequest , fmt .Errorf ("invalid UUID format" )))
221
- default :
222
- return c .JSON (model .NewHTTPError (http .StatusInternalServerError , err ))
223
- }
224
- }
225
- return c .JSON (model .NewHTTPError (http .StatusInternalServerError , err ))
239
+ return f .handleDaoError (c , err )
226
240
}
227
241
return c .JSON (http .StatusNoContent , nil )
228
242
}
@@ -251,10 +265,10 @@ func (f FlagAPIHandler) UpdateFeatureFlagStatus(c echo.Context) error {
251
265
}
252
266
253
267
flag .Disable = & statusUpdate .Disable
254
- flag .LastUpdatedDate = time .Now ()
268
+ flag .LastUpdatedDate = f . options . Clock .Now ()
255
269
err = f .dao .UpdateFlag (c .Request ().Context (), flag )
256
270
if err != nil {
257
- return c . JSON ( model . NewHTTPError ( http . StatusInternalServerError , err ) )
271
+ return f . handleDaoError ( c , err )
258
272
}
259
273
return c .JSON (http .StatusOK , flag )
260
274
}
@@ -263,7 +277,7 @@ func (f FlagAPIHandler) UpdateFeatureFlagStatus(c echo.Context) error {
263
277
func (f FlagAPIHandler ) handleDaoError (c echo.Context , err daoErr.DaoError ) error {
264
278
switch err .Code () {
265
279
case daoErr .NotFound :
266
- return c .JSON (model .NewHTTPError (http .StatusNotFound , fmt .Errorf ("flag with id %s not found" , c . Param ( "id" ) )))
280
+ return c .JSON (model .NewHTTPError (http .StatusNotFound , fmt .Errorf ("flag not found" )))
267
281
case daoErr .InvalidUUID :
268
282
return c .JSON (model .NewHTTPError (http .StatusBadRequest , fmt .Errorf ("invalid UUID format" )))
269
283
default :
0 commit comments