Skip to content

Commit 1528040

Browse files
authored
feat: add query filters for envelopes (#281)
1 parent f387f0c commit 1528040

File tree

8 files changed

+263
-3
lines changed

8 files changed

+263
-3
lines changed

api/docs.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,26 @@ const docTemplate = `{
380380
"Allocations"
381381
],
382382
"summary": "Get allocations",
383+
"parameters": [
384+
{
385+
"type": "string",
386+
"description": "Filter by month",
387+
"name": "month",
388+
"in": "query"
389+
},
390+
{
391+
"type": "string",
392+
"description": "Filter by amount",
393+
"name": "amount",
394+
"in": "query"
395+
},
396+
{
397+
"type": "string",
398+
"description": "Filter by envelope ID",
399+
"name": "envelope",
400+
"in": "query"
401+
}
402+
],
383403
"responses": {
384404
"200": {
385405
"description": "OK",
@@ -955,6 +975,26 @@ const docTemplate = `{
955975
"Categories"
956976
],
957977
"summary": "Get categories",
978+
"parameters": [
979+
{
980+
"type": "string",
981+
"description": "Filter by name",
982+
"name": "name",
983+
"in": "query"
984+
},
985+
{
986+
"type": "string",
987+
"description": "Filter by note",
988+
"name": "note",
989+
"in": "query"
990+
},
991+
{
992+
"type": "string",
993+
"description": "Filter by budget ID",
994+
"name": "budget",
995+
"in": "query"
996+
}
997+
],
958998
"responses": {
959999
"200": {
9601000
"description": "OK",
@@ -1219,6 +1259,26 @@ const docTemplate = `{
12191259
"Envelopes"
12201260
],
12211261
"summary": "Get envelopes",
1262+
"parameters": [
1263+
{
1264+
"type": "string",
1265+
"description": "Filter by name",
1266+
"name": "name",
1267+
"in": "query"
1268+
},
1269+
{
1270+
"type": "string",
1271+
"description": "Filter by note",
1272+
"name": "note",
1273+
"in": "query"
1274+
},
1275+
{
1276+
"type": "string",
1277+
"description": "Filter by category ID",
1278+
"name": "category",
1279+
"in": "query"
1280+
}
1281+
],
12221282
"responses": {
12231283
"200": {
12241284
"description": "OK",

api/swagger.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,26 @@
368368
"Allocations"
369369
],
370370
"summary": "Get allocations",
371+
"parameters": [
372+
{
373+
"type": "string",
374+
"description": "Filter by month",
375+
"name": "month",
376+
"in": "query"
377+
},
378+
{
379+
"type": "string",
380+
"description": "Filter by amount",
381+
"name": "amount",
382+
"in": "query"
383+
},
384+
{
385+
"type": "string",
386+
"description": "Filter by envelope ID",
387+
"name": "envelope",
388+
"in": "query"
389+
}
390+
],
371391
"responses": {
372392
"200": {
373393
"description": "OK",
@@ -943,6 +963,26 @@
943963
"Categories"
944964
],
945965
"summary": "Get categories",
966+
"parameters": [
967+
{
968+
"type": "string",
969+
"description": "Filter by name",
970+
"name": "name",
971+
"in": "query"
972+
},
973+
{
974+
"type": "string",
975+
"description": "Filter by note",
976+
"name": "note",
977+
"in": "query"
978+
},
979+
{
980+
"type": "string",
981+
"description": "Filter by budget ID",
982+
"name": "budget",
983+
"in": "query"
984+
}
985+
],
946986
"responses": {
947987
"200": {
948988
"description": "OK",
@@ -1207,6 +1247,26 @@
12071247
"Envelopes"
12081248
],
12091249
"summary": "Get envelopes",
1250+
"parameters": [
1251+
{
1252+
"type": "string",
1253+
"description": "Filter by name",
1254+
"name": "name",
1255+
"in": "query"
1256+
},
1257+
{
1258+
"type": "string",
1259+
"description": "Filter by note",
1260+
"name": "note",
1261+
"in": "query"
1262+
},
1263+
{
1264+
"type": "string",
1265+
"description": "Filter by category ID",
1266+
"name": "category",
1267+
"in": "query"
1268+
}
1269+
],
12101270
"responses": {
12111271
"200": {
12121272
"description": "OK",

api/swagger.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,19 @@ paths:
815815
/v1/allocations:
816816
get:
817817
description: Returns a list of allocations
818+
parameters:
819+
- description: Filter by month
820+
in: query
821+
name: month
822+
type: string
823+
- description: Filter by amount
824+
in: query
825+
name: amount
826+
type: string
827+
- description: Filter by envelope ID
828+
in: query
829+
name: envelope
830+
type: string
818831
produces:
819832
- application/json
820833
responses:
@@ -1202,6 +1215,19 @@ paths:
12021215
/v1/categories:
12031216
get:
12041217
description: Returns a list of categories
1218+
parameters:
1219+
- description: Filter by name
1220+
in: query
1221+
name: name
1222+
type: string
1223+
- description: Filter by note
1224+
in: query
1225+
name: note
1226+
type: string
1227+
- description: Filter by budget ID
1228+
in: query
1229+
name: budget
1230+
type: string
12051231
produces:
12061232
- application/json
12071233
responses:
@@ -1380,6 +1406,19 @@ paths:
13801406
/v1/envelopes:
13811407
get:
13821408
description: Returns a list of envelopes
1409+
parameters:
1410+
- description: Filter by name
1411+
in: query
1412+
name: name
1413+
type: string
1414+
- description: Filter by note
1415+
in: query
1416+
name: note
1417+
type: string
1418+
- description: Filter by category ID
1419+
in: query
1420+
name: category
1421+
type: string
13831422
produces:
13841423
- application/json
13851424
responses:

pkg/controllers/allocation.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ func CreateAllocation(c *gin.Context) {
144144
// @Failure 404
145145
// @Failure 500 {object} httputil.HTTPError
146146
// @Router /v1/allocations [get]
147+
// @Param month query string false "Filter by month"
148+
// @Param amount query string false "Filter by amount"
149+
// @Param envelope query string false "Filter by envelope ID"
147150
func GetAllocations(c *gin.Context) {
148151
var filter AllocationQueryFilter
149152
if err := c.Bind(&filter); err != nil {

pkg/controllers/category.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ func CreateCategory(c *gin.Context) {
140140
// @Failure 404
141141
// @Failure 500 {object} httputil.HTTPError
142142
// @Router /v1/categories [get]
143+
// @Param name query string false "Filter by name"
144+
// @Param note query string false "Filter by note"
145+
// @Param budget query string false "Filter by budget ID"
143146
func GetCategories(c *gin.Context) {
144147
var filter CategoryQueryFilter
145148

pkg/controllers/envelope.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,25 @@ type EnvelopeLinks struct {
3636
Transactions string `json:"transactions" example:"https://example.com/api/v1/transactions?envelope=45b6b5b9-f746-4ae9-b77b-7688b91f8166"`
3737
}
3838

39+
type EnvelopeQueryFilter struct {
40+
Name string `form:"name"`
41+
CategoryID string `form:"category"`
42+
Note string `form:"note"`
43+
}
44+
45+
func (e EnvelopeQueryFilter) ToCreate(c *gin.Context) (models.EnvelopeCreate, error) {
46+
categoryID, err := httputil.UUIDFromString(c, e.CategoryID)
47+
if err != nil {
48+
return models.EnvelopeCreate{}, err
49+
}
50+
51+
return models.EnvelopeCreate{
52+
Name: e.Name,
53+
Note: e.Note,
54+
CategoryID: categoryID,
55+
}, nil
56+
}
57+
3958
// RegisterEnvelopeRoutes registers the routes for envelopes with
4059
// the RouterGroup that is passed.
4160
func RegisterEnvelopeRoutes(r *gin.RouterGroup) {
@@ -124,10 +143,27 @@ func CreateEnvelope(c *gin.Context) {
124143
// @Failure 404
125144
// @Failure 500 {object} httputil.HTTPError
126145
// @Router /v1/envelopes [get]
146+
// @Param name query string false "Filter by name"
147+
// @Param note query string false "Filter by note"
148+
// @Param category query string false "Filter by category ID"
127149
func GetEnvelopes(c *gin.Context) {
128-
var envelopes []models.Envelope
150+
var filter EnvelopeQueryFilter
151+
152+
// The filters contain only strings, so this will always succeed
153+
_ = c.Bind(&filter)
154+
155+
queryFields := httputil.GetURLFields(c.Request.URL, filter)
156+
157+
// Convert the QueryFilter to a Create struct
158+
create, err := filter.ToCreate(c)
159+
if err != nil {
160+
return
161+
}
129162

130-
database.DB.Find(&envelopes)
163+
var envelopes []models.Envelope
164+
database.DB.Where(&models.Envelope{
165+
EnvelopeCreate: create,
166+
}, queryFields...).Find(&envelopes)
131167

132168
// When there are no resources, we want an empty list, not null
133169
// Therefore, we use make to create a slice with zero elements

pkg/controllers/envelope_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,65 @@ func (suite *TestSuiteEnv) TestGetEnvelopes() {
5959
assert.LessOrEqual(suite.T(), diff, test.TOLERANCE)
6060
}
6161

62+
func (suite *TestSuiteEnv) TestGetEnvelopesInvalidQuery() {
63+
tests := []string{
64+
"category=DefinitelyACat",
65+
}
66+
67+
for _, tt := range tests {
68+
suite.T().Run(tt, func(t *testing.T) {
69+
recorder := test.Request(suite.T(), http.MethodGet, fmt.Sprintf("http://example.com/v1/envelopes?%s", tt), "")
70+
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &recorder)
71+
})
72+
}
73+
}
74+
75+
func (suite *TestSuiteEnv) TestGetEnvelopesFilter() {
76+
c1 := createTestCategory(suite.T(), models.CategoryCreate{})
77+
c2 := createTestCategory(suite.T(), models.CategoryCreate{})
78+
79+
_ = createTestEnvelope(suite.T(), models.EnvelopeCreate{
80+
Name: "Groceries",
81+
Note: "For the stuff bought in supermarkets",
82+
CategoryID: c1.Data.ID,
83+
})
84+
85+
_ = createTestEnvelope(suite.T(), models.EnvelopeCreate{
86+
Name: "Hairdresser",
87+
Note: "Because… Hair!",
88+
CategoryID: c2.Data.ID,
89+
})
90+
91+
_ = createTestEnvelope(suite.T(), models.EnvelopeCreate{
92+
Name: "Stamps",
93+
Note: "Because each stamp needs to go on an envelope",
94+
CategoryID: c2.Data.ID,
95+
})
96+
97+
tests := []struct {
98+
name string
99+
query string
100+
len int
101+
}{
102+
{"Category 2", fmt.Sprintf("category=%s", c2.Data.ID), 2},
103+
{"Category Not Existing", "category=e0f9ff7a-9f07-463c-bbd2-0d72d09d3cc6", 0},
104+
{"Empty Note", "note=", 0},
105+
{"Emtpy Name", "name=", 0},
106+
{"Name & Note", "name=Groceries&note=For the stuff bought in supermarkets", 1},
107+
}
108+
109+
for _, tt := range tests {
110+
suite.T().Run(tt.name, func(t *testing.T) {
111+
var re controllers.EnvelopeListResponse
112+
r := test.Request(t, http.MethodGet, fmt.Sprintf("/v1/envelopes?%s", tt.query), "")
113+
test.AssertHTTPStatus(t, http.StatusOK, &r)
114+
test.DecodeResponse(t, &r, &re)
115+
116+
assert.Equal(t, tt.len, len(re.Data), "Request ID: %s", r.Result().Header.Get("x-request-id"))
117+
})
118+
}
119+
}
120+
62121
func (suite *TestSuiteEnv) TestGetEnvelope() {
63122
envelope := createTestEnvelope(suite.T(), models.EnvelopeCreate{})
64123
recorder := test.Request(suite.T(), http.MethodGet, envelope.Data.Links.Self, "")

pkg/test/helpers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func AssertHTTPStatus(t *testing.T, expected int, r *httptest.ResponseRecorder)
6868
func DecodeResponse(t *testing.T, r *httptest.ResponseRecorder, target interface{}) {
6969
err := json.NewDecoder(r.Body).Decode(target)
7070
if err != nil {
71-
assert.FailNow(t, "Parsing error", "Unable to parse response from server %q into %v, '%v'", r.Body, reflect.TypeOf(target), err)
71+
assert.FailNow(t, "Parsing error", "Unable to parse response from server %q into %v, '%v', Request ID: %s", r.Body, reflect.TypeOf(target), err, r.Result().Header.Get("x-request-id"))
7272
}
7373
}
7474

0 commit comments

Comments
 (0)