Skip to content

Commit d54b5dd

Browse files
authored
Merge pull request #8 from solomonng2001/solomon/pagination-filtering-sorting
Solomon/pagination filtering sorting
2 parents 99a172a + 8d23dde commit d54b5dd

File tree

7 files changed

+606
-9
lines changed

7 files changed

+606
-9
lines changed

services/question-service/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,11 @@ The server will be available at http://localhost:8080.
5454
- `GET /questions`
5555
- `PUT /questions/{id}`
5656
- `DELETE /questions/{id}`
57+
58+
## Managing Firebase
59+
60+
To reset and repopulate the database, run the following command:
61+
62+
```bash
63+
go run populate.go
64+
```

services/question-service/handlers/create.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,11 @@ func (s *Service) CreateQuestion(w http.ResponseWriter, r *http.Request) {
7373
}
7474

7575
// Map data
76-
question.DocRefID = doc.Ref.ID
7776
if err := doc.DataTo(&question); err != nil {
7877
http.Error(w, "Failed to map question data", http.StatusInternalServerError)
7978
return err
8079
}
80+
question.DocRefID = doc.Ref.ID
8181

8282
return nil
8383
})

services/question-service/handlers/list.go

Lines changed: 256 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,149 @@
11
package handlers
22

33
import (
4+
"cloud.google.com/go/firestore"
45
"encoding/json"
6+
"google.golang.org/api/iterator"
57
"net/http"
68
"question-service/models"
7-
8-
"google.golang.org/api/iterator"
9+
"strconv"
10+
"strings"
911
)
1012

11-
// TODO: add filters/pagination/sorter
13+
var isValidSortField = map[string]bool{
14+
"title": true,
15+
"id": true,
16+
"complexity": true,
17+
"createdAt": true,
18+
}
19+
20+
var isValidSortValue = map[string]bool{
21+
"asc": true,
22+
"desc": true,
23+
}
24+
25+
var getOrderFromSortValue = map[string]firestore.Direction{
26+
"asc": firestore.Asc,
27+
"desc": firestore.Desc,
28+
}
29+
30+
var isValidQueryField = map[string]bool{
31+
"title": true,
32+
"complexity": true,
33+
"categories": true,
34+
"sortField": true,
35+
"sortValue": true,
36+
"limit": true,
37+
"offset": true,
38+
"id": true,
39+
"createdAt": true,
40+
}
41+
1242
func (s *Service) ListQuestions(w http.ResponseWriter, r *http.Request) {
1343
ctx := r.Context()
14-
iter := s.Client.Collection("questions").Documents(ctx)
44+
45+
// Check validity of query parameters
46+
for key := range r.URL.Query() {
47+
if _, valid := isValidQueryField[key]; !valid {
48+
http.Error(w, "Invalid query parameter: "+key, http.StatusBadRequest)
49+
return
50+
}
51+
}
52+
53+
query := s.Client.Collection("questions").Query
54+
55+
// Filtering by title
56+
titleParam := r.URL.Query().Get("title")
57+
if titleParam != "" {
58+
query = query.Where("title", "==", titleParam)
59+
}
60+
61+
// Filtering by complexity (multi-select)
62+
complexityParam := r.URL.Query().Get("complexity")
63+
if complexityParam != "" {
64+
complexities := strings.Split(complexityParam, ",")
65+
query = query.Where("complexity", "in", complexities)
66+
}
67+
68+
// Filtering by categories (multi-select)
69+
categoriesParam := r.URL.Query().Get("categories")
70+
if categoriesParam != "" {
71+
categories := strings.Split(categoriesParam, ",")
72+
query = query.Where("categories", "array-contains-any", categories)
73+
}
74+
75+
// Sorting
76+
sortFieldsParam := r.URL.Query()["sortField"]
77+
sortValuesParam := r.URL.Query()["sortValue"]
78+
79+
if len(sortFieldsParam) != len(sortValuesParam) {
80+
http.Error(w, "Mismatched sortField and sortValue parameters", http.StatusBadRequest)
81+
return
82+
}
83+
84+
if len(sortFieldsParam) > 1 || len(sortValuesParam) > 1 {
85+
http.Error(w, "At most 1 sortField and sortValue pair allowed", http.StatusBadRequest)
86+
return
87+
}
88+
89+
sortedById := false
90+
for i, sortField := range sortFieldsParam {
91+
if !isValidSortField[sortField] {
92+
http.Error(w, "Invalid sortField: "+sortField, http.StatusBadRequest)
93+
return
94+
}
95+
96+
sortValue := sortValuesParam[i]
97+
if !isValidSortValue[sortValue] {
98+
http.Error(w, "Invalid sortValue: "+sortValue, http.StatusBadRequest)
99+
return
100+
}
101+
102+
query = query.OrderBy(sortField, getOrderFromSortValue[sortValue])
103+
104+
if sortField == "id" {
105+
sortedById = true
106+
}
107+
}
108+
if !sortedById {
109+
query = query.OrderBy("id", firestore.Asc)
110+
}
111+
112+
// Count total documents matching the filter
113+
totalIter, err := query.Documents(ctx).GetAll()
114+
totalCount := len(totalIter)
115+
if err != nil {
116+
http.Error(w, "Failed to count questions: "+err.Error(), http.StatusInternalServerError)
117+
return
118+
}
119+
120+
// Pagination
121+
limitParam := r.URL.Query().Get("limit")
122+
limit := 10
123+
if limitParam != "" {
124+
limit, err = strconv.Atoi(limitParam) // convert limit to integer
125+
if err != nil || limit <= 0 {
126+
http.Error(w, "Invalid limit", http.StatusBadRequest)
127+
return
128+
}
129+
}
130+
currentPage := 1
131+
offsetParam := r.URL.Query().Get("offset")
132+
if offsetParam != "" {
133+
offset, err := strconv.Atoi(offsetParam) // convert offset to integer
134+
if err != nil {
135+
http.Error(w, "Invalid offset", http.StatusBadRequest)
136+
return
137+
}
138+
if offset%limit != 0 {
139+
http.Error(w, "Offset does not match limit. Default limit is 10 when unspecified", http.StatusBadRequest)
140+
}
141+
currentPage = (offset / limit) + 1
142+
query = query.Offset(offset)
143+
}
144+
query = query.Limit(limit)
145+
146+
iter := query.Documents(ctx)
15147

16148
var questions []models.Question
17149
for {
@@ -27,14 +159,132 @@ func (s *Service) ListQuestions(w http.ResponseWriter, r *http.Request) {
27159

28160
// Map data
29161
var question models.Question
30-
question.DocRefID = doc.Ref.ID
31162
if err := doc.DataTo(&question); err != nil {
32163
http.Error(w, "Failed to parse question", http.StatusInternalServerError)
33164
return
34165
}
166+
question.DocRefID = doc.Ref.ID
35167
questions = append(questions, question)
36168
}
37169

170+
// Calculate pagination info
171+
totalPages := (totalCount + limit - 1) / limit
172+
hasNextPage := totalPages > currentPage
173+
174+
// Construct response
175+
response := models.QuestionResponse{
176+
TotalCount: totalCount,
177+
TotalPages: totalPages,
178+
CurrentPage: currentPage,
179+
Limit: limit,
180+
HasNextPage: hasNextPage,
181+
Questions: questions,
182+
}
183+
38184
w.Header().Set("Content-Type", "application/json")
39-
json.NewEncoder(w).Encode(questions)
185+
json.NewEncoder(w).Encode(response)
40186
}
187+
188+
//Manual test cases
189+
//
190+
//curl -X GET "http://localhost:8080/questions"
191+
//
192+
//curl -X GET "http://localhost:8080/questions?offset=10"
193+
//
194+
//
195+
//curl -X GET "http://localhost:8080/questions?title=Reverse%20a%20String"
196+
//
197+
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium"
198+
//
199+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures"
200+
//
201+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=Easy,Medium"
202+
//
203+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Strings&title=Reverse%20a%20String"
204+
//
205+
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String"
206+
//
207+
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&categories=Algorithms,Strings"
208+
//
209+
//
210+
//curl -X GET "http://localhost:8080/questions?sortField=title&sortValue=asc&offset=10"
211+
//
212+
//curl -X GET "http://localhost:8080/questions?sortField=createdAt&sortValue=desc&offset=10"
213+
//
214+
//curl -X GET "http://localhost:8080/questions?sortField=complexity&sortValue=desc&offset=10"
215+
//
216+
//curl -X GET "http://localhost:8080/questions?sortField=id&sortValue=asc&offset=10"
217+
//
218+
//curl -X GET "http://localhost:8080/questions?limit=5"
219+
//
220+
//curl -X GET "http://localhost:8080/questions?limit=5&offset=10"
221+
//
222+
//
223+
//curl -X GET "http://localhost:8080/questions?title=Reverse%20a%20String&sortField=complexity&sortValue=desc"
224+
//
225+
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&sortField=complexity&sortValue=desc"
226+
//
227+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&sortField=complexity&sortValue=desc"
228+
//
229+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=Easy,Medium&sortField=complexity&sortValue=desc"
230+
//
231+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Strings&title=Reverse%20a%20String&sortField=complexity&sortValue=desc"
232+
//
233+
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&sortField=complexity&sortValue=desc"
234+
//
235+
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=complexity&sortValue=desc"
236+
//
237+
//
238+
//curl -X GET "http://localhost:8080/questions?title=Reverse%20a%20String&sortField=createdAt&sortValue=desc"
239+
//
240+
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&sortField=createdAt&sortValue=desc"
241+
//
242+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&sortField=createdAt&sortValue=desc"
243+
//
244+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=Easy,Medium&sortField=createdAt&sortValue=desc"
245+
//
246+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Strings&title=Reverse%20a%20String&sortField=createdAt&sortValue=desc"
247+
//
248+
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&sortField=createdAt&sortValue=desc"
249+
//
250+
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=createdAt&sortValue=desc"
251+
//
252+
//
253+
//curl -X GET "http://localhost:8080/questions?title=Reverse%20a%20String&sortField=id&sortValue=asc"
254+
//
255+
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&sortField=id&sortValue=asc"
256+
//
257+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&sortField=id&sortValue=asc"
258+
//
259+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=Easy,Medium&sortField=id&sortValue=asc"
260+
//
261+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Strings&title=Reverse%20a%20String&sortField=id&sortValue=asc"
262+
//
263+
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&sortField=id&sortValue=asc"
264+
//
265+
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=id&sortValue=asc"
266+
//
267+
//
268+
//curl -X GET "http://localhost:8080/questions?title=Reverse%20a%20String&sortField=title&sortValue=asc"
269+
//
270+
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&sortField=title&sortValue=asc"
271+
//
272+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&sortField=title&sortValue=asc"
273+
//
274+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=Easy,Medium&sortField=title&sortValue=asc"
275+
//
276+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Strings&title=Reverse%20a%20String&sortField=title&sortValue=asc"
277+
//
278+
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&sortField=title&sortValue=asc"
279+
//
280+
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=title&sortValue=asc"
281+
//
282+
//
283+
//curl -X GET "http://localhost:8080/questions?complexity=InvalidComplexity"
284+
//
285+
//curl -X GET "http://localhost:8080/questions?InvalidFilterField=InvalidComplexity"
286+
//
287+
//curl -X GET "http://localhost:8080/questions?sortField=InvalidSortField&sortValue=asc"
288+
//
289+
//curl -X GET "http://localhost:8080/questions?sortField=complexity&&sortValue=InvalidSortValue"
290+
//

services/question-service/handlers/read.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ func (s *Service) ReadQuestion(w http.ResponseWriter, r *http.Request) {
3131

3232
// Map data
3333
var question models.Question
34-
question.DocRefID = doc.Ref.ID
3534
if err := doc.DataTo(&question); err != nil {
3635
http.Error(w, "Failed to map question data", http.StatusInternalServerError)
3736
return
3837
}
38+
question.DocRefID = doc.Ref.ID
3939

4040
w.Header().Set("Content-Type", "application/json")
4141
json.NewEncoder(w).Encode(question)

services/question-service/handlers/update.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,11 @@ func (s *Service) UpdateQuestion(w http.ResponseWriter, r *http.Request) {
6767
}
6868

6969
// Map data
70-
question.DocRefID = doc.Ref.ID
7170
if err := doc.DataTo(&question); err != nil {
7271
http.Error(w, "Failed to map question data", http.StatusInternalServerError)
7372
return
7473
}
74+
question.DocRefID = doc.Ref.ID
7575

7676
w.Header().Set("Content-Type", "application/json")
7777
json.NewEncoder(w).Encode(question)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package models
2+
3+
type QuestionResponse struct {
4+
TotalCount int `json:"totalCount"`
5+
TotalPages int `json:"totalPages"`
6+
CurrentPage int `json:"currentPage"`
7+
Limit int `json:"limit"`
8+
HasNextPage bool `json:"hasNextPage"`
9+
Questions []Question `json:"questions"`
10+
}

0 commit comments

Comments
 (0)