Skip to content

Commit 58640b8

Browse files
committed
Merge remote-tracking branch 'origin/staging' into ben/integrate-backend-api
Merge new changes in staging into local branch
2 parents 259b05f + d6f2126 commit 58640b8

File tree

4 files changed

+187
-77
lines changed

4 files changed

+187
-77
lines changed

services/question-service/handlers/list.go

Lines changed: 101 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package handlers
33
import (
44
"cloud.google.com/go/firestore"
55
"encoding/json"
6-
"google.golang.org/api/iterator"
76
"net/http"
87
"question-service/models"
98
"strconv"
@@ -61,14 +60,24 @@ func (s *Service) ListQuestions(w http.ResponseWriter, r *http.Request) {
6160
// Filtering by complexity (multi-select)
6261
complexityParam := r.URL.Query().Get("complexity")
6362
if complexityParam != "" {
64-
complexities := strings.Split(complexityParam, ",")
65-
query = query.Where("complexity", "in", complexities)
63+
var complexityInts []int
64+
complexityStrs := strings.Split(complexityParam, ",")
65+
for _, complexityStr := range complexityStrs {
66+
complexityType, err := models.ParseComplexity(complexityStr)
67+
if err != nil {
68+
http.Error(w, "Failed to filter by complexity: "+err.Error(), http.StatusInternalServerError)
69+
return
70+
}
71+
complexityInts = append(complexityInts, int(complexityType))
72+
}
73+
query = query.Where("complexity", "in", complexityInts)
6674
}
6775

68-
// Filtering by categories (multi-select)
76+
// Filtering by categories (multi-select): Partially using array-contains-any first
6977
categoriesParam := r.URL.Query().Get("categories")
78+
var categories []string
7079
if categoriesParam != "" {
71-
categories := strings.Split(categoriesParam, ",")
80+
categories = strings.Split(categoriesParam, ",")
7281
query = query.Where("categories", "array-contains-any", categories)
7382
}
7483

@@ -109,12 +118,33 @@ func (s *Service) ListQuestions(w http.ResponseWriter, r *http.Request) {
109118
query = query.OrderBy("id", firestore.Asc)
110119
}
111120

112-
// Count total documents matching the filter
113-
totalIter, err := query.Documents(ctx).GetAll()
114-
totalCount := len(totalIter)
121+
results, err := query.Documents(ctx).GetAll()
115122
if err != nil {
116-
http.Error(w, "Failed to count questions: "+err.Error(), http.StatusInternalServerError)
117-
return
123+
http.Error(w, "Failed to to get fetch questions: "+err.Error(), http.StatusInternalServerError)
124+
}
125+
126+
// Filter the results to check if the document contains all categories (implementation of array-contains-all)
127+
var filteredResults []*firestore.DocumentSnapshot
128+
if categories != nil {
129+
for _, doc := range results {
130+
data := doc.Data()
131+
132+
// Retrieve the "categories" field from the document and convert to []string
133+
if docCategories, ok := data["categories"].([]interface{}); ok {
134+
stringCategories := make([]string, len(docCategories))
135+
for i, cat := range docCategories {
136+
if catStr, ok := cat.(string); ok {
137+
stringCategories[i] = catStr
138+
}
139+
}
140+
141+
if containsAllCategoriesSet(stringCategories, categories) {
142+
filteredResults = append(filteredResults, doc)
143+
}
144+
}
145+
}
146+
} else {
147+
filteredResults = results
118148
}
119149

120150
// Pagination
@@ -123,52 +153,42 @@ func (s *Service) ListQuestions(w http.ResponseWriter, r *http.Request) {
123153
if limitParam != "" {
124154
limit, err = strconv.Atoi(limitParam) // convert limit to integer
125155
if err != nil || limit <= 0 {
126-
http.Error(w, "Invalid limit", http.StatusBadRequest)
156+
http.Error(w, "Invalid limit: "+strconv.Itoa(limit), http.StatusBadRequest)
127157
return
128158
}
129159
}
130-
currentPage := 1
131160
offsetParam := r.URL.Query().Get("offset")
161+
var offset int
132162
if offsetParam != "" {
133-
offset, err := strconv.Atoi(offsetParam) // convert offset to integer
163+
offset, err = strconv.Atoi(offsetParam) // convert offset to integer
134164
if err != nil {
135-
http.Error(w, "Invalid offset", http.StatusBadRequest)
165+
http.Error(w, "Invalid offset: "+strconv.Itoa(offset), http.StatusBadRequest)
136166
return
137167
}
138168
if offset%limit != 0 {
139-
http.Error(w, "Offset does not match limit. Default limit is 10 when unspecified", http.StatusBadRequest)
169+
http.Error(w, "Offset does not match limit. Default limit is 10 when offset is unspecified",
170+
http.StatusBadRequest)
140171
}
141-
currentPage = (offset / limit) + 1
142-
query = query.Offset(offset)
143172
}
144-
query = query.Limit(limit)
145173

146-
iter := query.Documents(ctx)
174+
paginatedResults := paginateResults(filteredResults, offset, limit)
147175

148176
var questions []models.Question
149-
for {
150-
// Get data
151-
doc, err := iter.Next()
152-
if err == iterator.Done {
153-
break
154-
}
155-
if err != nil {
156-
http.Error(w, "Failed to retrieve questions", http.StatusInternalServerError)
157-
return
158-
}
159-
177+
for _, doc := range paginatedResults {
160178
// Map data
161179
var question models.Question
162180
if err := doc.DataTo(&question); err != nil {
163-
http.Error(w, "Failed to parse question", http.StatusInternalServerError)
181+
http.Error(w, "Failed to parse question: "+err.Error(), http.StatusInternalServerError)
164182
return
165183
}
166184
question.DocRefID = doc.Ref.ID
167185
questions = append(questions, question)
168186
}
169187

170188
// Calculate pagination info
189+
totalCount := len(filteredResults)
171190
totalPages := (totalCount + limit - 1) / limit
191+
currentPage := (offset / limit) + 1
172192
hasNextPage := totalPages > currentPage
173193

174194
// Construct response
@@ -185,6 +205,36 @@ func (s *Service) ListQuestions(w http.ResponseWriter, r *http.Request) {
185205
json.NewEncoder(w).Encode(response)
186206
}
187207

208+
func containsAllCategoriesSet(docCategories []string, queryCategories []string) bool {
209+
categorySet := make(map[string]bool)
210+
211+
// Populate the set with document categories
212+
for _, cat := range docCategories {
213+
categorySet[cat] = true
214+
}
215+
216+
// Check if all query categories are present in the document categories
217+
for _, queryCat := range queryCategories {
218+
if !categorySet[queryCat] {
219+
return false
220+
}
221+
}
222+
return true
223+
}
224+
225+
func paginateResults(results []*firestore.DocumentSnapshot, offset, limit int) []*firestore.DocumentSnapshot {
226+
start := offset
227+
end := offset + limit
228+
229+
if start >= len(results) {
230+
return []*firestore.DocumentSnapshot{}
231+
}
232+
if end > len(results) {
233+
end = len(results)
234+
}
235+
return results[start:end]
236+
}
237+
188238
//Manual test cases
189239
//
190240
//curl -X GET "http://localhost:8080/questions"
@@ -194,17 +244,17 @@ func (s *Service) ListQuestions(w http.ResponseWriter, r *http.Request) {
194244
//
195245
//curl -X GET "http://localhost:8080/questions?title=Reverse%20a%20String"
196246
//
197-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium"
247+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium"
198248
//
199249
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures"
200250
//
201-
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=Easy,Medium"
251+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=easy,medium"
202252
//
203253
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Strings&title=Reverse%20a%20String"
204254
//
205-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String"
255+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String"
206256
//
207-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&categories=Algorithms,Strings"
257+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&categories=Algorithms,Strings"
208258
//
209259
//
210260
//curl -X GET "http://localhost:8080/questions?sortField=title&sortValue=asc&offset=10"
@@ -222,62 +272,62 @@ func (s *Service) ListQuestions(w http.ResponseWriter, r *http.Request) {
222272
//
223273
//curl -X GET "http://localhost:8080/questions?title=Reverse%20a%20String&sortField=complexity&sortValue=desc"
224274
//
225-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&sortField=complexity&sortValue=desc"
275+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&sortField=complexity&sortValue=desc"
226276
//
227277
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&sortField=complexity&sortValue=desc"
228278
//
229-
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=Easy,Medium&sortField=complexity&sortValue=desc"
279+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=easy,medium&sortField=complexity&sortValue=desc"
230280
//
231281
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Strings&title=Reverse%20a%20String&sortField=complexity&sortValue=desc"
232282
//
233-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&sortField=complexity&sortValue=desc"
283+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&sortField=complexity&sortValue=desc"
234284
//
235-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=complexity&sortValue=desc"
285+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=complexity&sortValue=desc"
236286
//
237287
//
238288
//curl -X GET "http://localhost:8080/questions?title=Reverse%20a%20String&sortField=createdAt&sortValue=desc"
239289
//
240-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&sortField=createdAt&sortValue=desc"
290+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&sortField=createdAt&sortValue=desc"
241291
//
242292
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&sortField=createdAt&sortValue=desc"
243293
//
244-
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=Easy,Medium&sortField=createdAt&sortValue=desc"
294+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=easy,medium&sortField=createdAt&sortValue=desc"
245295
//
246296
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Strings&title=Reverse%20a%20String&sortField=createdAt&sortValue=desc"
247297
//
248-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&sortField=createdAt&sortValue=desc"
298+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&sortField=createdAt&sortValue=desc"
249299
//
250-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=createdAt&sortValue=desc"
300+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=createdAt&sortValue=desc"
251301
//
252302
//
253303
//curl -X GET "http://localhost:8080/questions?title=Reverse%20a%20String&sortField=id&sortValue=asc"
254304
//
255-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&sortField=id&sortValue=asc"
305+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&sortField=id&sortValue=asc"
256306
//
257307
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&sortField=id&sortValue=asc"
258308
//
259-
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=Easy,Medium&sortField=id&sortValue=asc"
309+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=easy,medium&sortField=id&sortValue=asc"
260310
//
261311
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Strings&title=Reverse%20a%20String&sortField=id&sortValue=asc"
262312
//
263-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&sortField=id&sortValue=asc"
313+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&sortField=id&sortValue=asc"
264314
//
265-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=id&sortValue=asc"
315+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=id&sortValue=asc"
266316
//
267317
//
268318
//curl -X GET "http://localhost:8080/questions?title=Reverse%20a%20String&sortField=title&sortValue=asc"
269319
//
270-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&sortField=title&sortValue=asc"
320+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&sortField=title&sortValue=asc"
271321
//
272322
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&sortField=title&sortValue=asc"
273323
//
274-
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=Easy,Medium&sortField=title&sortValue=asc"
324+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=easy,medium&sortField=title&sortValue=asc"
275325
//
276326
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Strings&title=Reverse%20a%20String&sortField=title&sortValue=asc"
277327
//
278-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&sortField=title&sortValue=asc"
328+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&sortField=title&sortValue=asc"
279329
//
280-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=title&sortValue=asc"
330+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=title&sortValue=asc"
281331
//
282332
//
283333
//curl -X GET "http://localhost:8080/questions?complexity=InvalidComplexity"
Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,76 @@
11
package models
22

3-
import "time"
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"time"
7+
)
8+
9+
type ComplexityType int
10+
11+
const (
12+
Easy ComplexityType = iota
13+
Medium
14+
Hard
15+
)
416

517
// TODO: currently the Question model is a simplified model
618
type Question struct {
7-
Title string `json:"title"`
8-
Description string `json:"description"`
9-
Categories []string `json:"categories"`
10-
Complexity string `json:"complexity"`
19+
Title string `json:"title"`
20+
Description string `json:"description"`
21+
Categories []string `json:"categories"`
22+
Complexity ComplexityType `json:"complexity"` // Now an enum
1123

1224
// Special DB fields
1325
ID int64 `json:"id"`
1426
DocRefID string `json:"docRefId"` // The firestore document reference ID
1527
CreatedAt time.Time `json:"createdAt"`
1628
}
29+
30+
func (c ComplexityType) String() string {
31+
return [...]string{"easy", "medium", "hard"}[c]
32+
}
33+
34+
func ParseComplexity(complexityStr string) (ComplexityType, error) {
35+
switch complexityStr {
36+
case "easy":
37+
return Easy, nil
38+
case "medium":
39+
return Medium, nil
40+
case "hard":
41+
return Hard, nil
42+
default:
43+
return Easy, fmt.Errorf("invalid complexity level: %s", complexityStr)
44+
}
45+
}
46+
47+
func (c ComplexityType) MarshalJSON() ([]byte, error) {
48+
return json.Marshal(c.String())
49+
}
50+
51+
func (c *ComplexityType) UnmarshalJSON(data []byte) error {
52+
var complexityStr string
53+
if err := json.Unmarshal(data, &complexityStr); err != nil {
54+
return err
55+
}
56+
57+
complexity, err := ParseComplexity(complexityStr)
58+
if err != nil {
59+
return err
60+
}
61+
62+
*c = complexity
63+
return nil
64+
}
65+
66+
func (c ComplexityType) MarshalFirestore() (interface{}, error) {
67+
return int(c), nil
68+
}
69+
70+
func (c *ComplexityType) UnmarshalFirestore(data interface{}) error {
71+
if complexityInt, ok := data.(int); ok {
72+
*c = ComplexityType(complexityInt)
73+
return nil
74+
}
75+
return fmt.Errorf("invalid type for ComplexityType in Firestore: %T", data)
76+
}

0 commit comments

Comments
 (0)