Skip to content

Commit 7c13cba

Browse files
committed
fix: implemented the product search functionality
✅ Added Search Endpoint: - Route: GET /api/products/search?q=<search_term> - Function: Searches products by name or description (case-insensitive) - Priority: Results with matching names appear first ✅ Implementation Details: 1. Repository Layer: Added Search() method to ProductRepository interface 2. Database Query: Uses ILIKE for case-insensitive search on name and description 3. Handler Layer: Added SearchProducts() handler with query parameter validation 4. Route Registration: Added search route to main.go (placed before the /{id} route to avoid conflicts) 5. Mock Updated: Updated repository mock for testing
1 parent aeb686c commit 7c13cba

File tree

9 files changed

+157
-15
lines changed

9 files changed

+157
-15
lines changed

cmd/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ func setupRoutes(
134134
apiV1.HandleFunc("/auth/register", ah.Register).Methods("POST")
135135
apiV1.HandleFunc("/auth/login", ah.Login).Methods("POST")
136136
apiV1.HandleFunc("/products", ph.GetAllProducts).Methods("GET")
137+
apiV1.HandleFunc("/products/search", ph.SearchProducts).Methods("GET")
137138
apiV1.HandleFunc("/products/{id:[0-9a-fA-F-]+}", ph.GetProduct).Methods("GET")
138139
apiV1.HandleFunc("/categories", ch.GetAllCategories).Methods("GET")
139140
apiV1.HandleFunc("/categories/{id:[0-9a-fA-F-]+}", ch.GetCategory).Methods("GET")

docker-compose.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
services:
2+
app:
3+
build: .
4+
ports:
5+
- "4444:4444"
6+
environment:
7+
- DATABASE_URL=postgres://postgres:password@db:5432/bulletcloud?sslmode=disable
8+
- PORT=4444
9+
- ENV=development
10+
- JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
11+
- JWT_ACCESS_EXPIRY=15m
12+
- JWT_REFRESH_EXPIRY=168h
13+
- CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:3002
14+
- BCRYPT_COST=12
15+
- MAX_LOGIN_ATTEMPTS=5
16+
- LOCKOUT_DURATION=30m
17+
- LOG_LEVEL=info
18+
- LOG_FORMAT=json
19+
depends_on:
20+
db:
21+
condition: service_healthy
22+
networks:
23+
- app-network
24+
25+
db:
26+
image: postgres:15-alpine
27+
environment:
28+
- POSTGRES_DB=bulletcloud
29+
- POSTGRES_USER=postgres
30+
- POSTGRES_PASSWORD=password
31+
ports:
32+
- "5432:5432"
33+
volumes:
34+
- postgres_data:/var/lib/postgresql/data
35+
healthcheck:
36+
test: ["CMD-SHELL", "pg_isready -U postgres -d bulletcloud"]
37+
interval: 10s
38+
timeout: 5s
39+
retries: 5
40+
start_period: 30s
41+
networks:
42+
- app-network
43+
44+
volumes:
45+
postgres_data:
46+
47+
networks:
48+
app-network:
49+
driver: bridge

go.mod

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,25 @@ go 1.23.0
44

55
toolchain go1.24.1
66

7-
require github.com/gorilla/mux v1.8.1
7+
require (
8+
github.com/golang-jwt/jwt/v5 v5.2.2
9+
github.com/google/uuid v1.6.0
10+
github.com/gorilla/mux v1.8.1
11+
github.com/jackc/pgx/v5 v5.7.4
12+
github.com/joho/godotenv v1.5.1
13+
github.com/stretchr/testify v1.10.0
14+
golang.org/x/crypto v0.37.0
15+
)
816

917
require (
1018
github.com/davecgh/go-spew v1.1.1 // indirect
11-
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
12-
github.com/google/uuid v1.6.0 // indirect
1319
github.com/jackc/pgpassfile v1.0.0 // indirect
1420
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
15-
github.com/jackc/pgx/v5 v5.7.4 // indirect
1621
github.com/jackc/puddle/v2 v2.2.2 // indirect
17-
github.com/joho/godotenv v1.5.1 // indirect
22+
github.com/kr/text v0.2.0 // indirect
1823
github.com/pmezard/go-difflib v1.0.0 // indirect
24+
github.com/rogpeppe/go-internal v1.14.1 // indirect
1925
github.com/stretchr/objx v0.5.2 // indirect
20-
github.com/stretchr/testify v1.10.0 // indirect
21-
golang.org/x/crypto v0.37.0 // indirect
2226
golang.org/x/sync v0.13.0 // indirect
2327
golang.org/x/text v0.24.0 // indirect
2428
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
12
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
23
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
34
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -17,28 +18,30 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo
1718
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
1819
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
1920
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
21+
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
22+
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
23+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
24+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
2025
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2126
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
27+
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
28+
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
2229
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
2330
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
2431
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
2532
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
2633
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
2734
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
2835
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
29-
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
30-
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
3136
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
3237
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
33-
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
34-
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
3538
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
3639
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
37-
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
38-
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
3940
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
4041
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
4142
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
43+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
44+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
4245
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
4346
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
4447
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/handlers/product_handler.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,24 @@ func (h *ProductHandler) GetAllProducts(w http.ResponseWriter, r *http.Request)
9292
webutils.WriteJSON(w, http.StatusOK, productList)
9393
}
9494

95+
// SearchProducts handles GET requests to search products by query.
96+
// This is often a public endpoint.
97+
func (h *ProductHandler) SearchProducts(w http.ResponseWriter, r *http.Request) {
98+
query := r.URL.Query().Get("q")
99+
if query == "" {
100+
webutils.ErrorJSON(w, errors.New("search query parameter 'q' is required"), http.StatusBadRequest)
101+
return
102+
}
103+
104+
productList, err := h.ProductRepo.Search(r.Context(), query)
105+
if err != nil {
106+
webutils.ErrorJSON(w, errors.New("failed to search products"), http.StatusInternalServerError)
107+
return
108+
}
109+
110+
webutils.WriteJSON(w, http.StatusOK, productList)
111+
}
112+
95113
// GetProduct handles GET requests for a specific product by ID.
96114
// This is often a public endpoint.
97115
func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) {

internal/products/repository.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type ProductRepository interface {
1919
Create(ctx context.Context, product *models.Product) (*models.Product, error)
2020
FindByID(ctx context.Context, id uuid.UUID) (*models.Product, error)
2121
FindAll(ctx context.Context /* TODO: Add filtering/pagination params */) ([]models.Product, error)
22+
Search(ctx context.Context, query string) ([]models.Product, error)
2223
Update(ctx context.Context, id uuid.UUID, product *models.Product) (*models.Product, error)
2324
Delete(ctx context.Context, id uuid.UUID) error
2425
}
@@ -120,6 +121,51 @@ func (r *postgresProductRepository) FindAll(ctx context.Context) ([]models.Produ
120121
return products, nil
121122
}
122123

124+
// Search retrieves products that match the search query (name or description).
125+
func (r *postgresProductRepository) Search(ctx context.Context, query string) ([]models.Product, error) {
126+
searchQuery := `
127+
SELECT id, name, description, price, category_id, created_at, updated_at
128+
FROM products
129+
WHERE name ILIKE $1 OR description ILIKE $1
130+
ORDER BY
131+
CASE
132+
WHEN name ILIKE $1 THEN 1
133+
ELSE 2
134+
END,
135+
created_at DESC
136+
`
137+
searchPattern := "%" + query + "%"
138+
rows, err := r.db.Query(ctx, searchQuery, searchPattern)
139+
if err != nil {
140+
return nil, err
141+
}
142+
defer rows.Close()
143+
144+
products := make([]models.Product, 0)
145+
for rows.Next() {
146+
var product models.Product
147+
err := rows.Scan(
148+
&product.ID,
149+
&product.Name,
150+
&product.Description,
151+
&product.Price,
152+
&product.CategoryID,
153+
&product.CreatedAt,
154+
&product.UpdatedAt,
155+
)
156+
if err != nil {
157+
return nil, err
158+
}
159+
products = append(products, product)
160+
}
161+
162+
if rows.Err() != nil {
163+
return nil, rows.Err()
164+
}
165+
166+
return products, nil
167+
}
168+
123169
// Update modifies an existing product in the database.
124170
func (r *postgresProductRepository) Update(ctx context.Context, id uuid.UUID, product *models.Product) (*models.Product, error) {
125171
query := `

internal/products/repository_mock.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,29 @@ func (_m *MockProductRepository) FindAll(ctx context.Context) ([]models.Product,
8282
return r0, r1
8383
}
8484

85+
// Search provides a mock function with given fields: ctx, query
86+
func (_m *MockProductRepository) Search(ctx context.Context, query string) ([]models.Product, error) {
87+
ret := _m.Called(ctx, query)
88+
89+
var r0 []models.Product
90+
if rf, ok := ret.Get(0).(func(context.Context, string) []models.Product); ok {
91+
r0 = rf(ctx, query)
92+
} else {
93+
if ret.Get(0) != nil {
94+
r0 = ret.Get(0).([]models.Product)
95+
}
96+
}
97+
98+
var r1 error
99+
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
100+
r1 = rf(ctx, query)
101+
} else {
102+
r1 = ret.Error(1)
103+
}
104+
105+
return r0, r1
106+
}
107+
85108
// Update provides a mock function with given fields: ctx, id, product
86109
func (_m *MockProductRepository) Update(ctx context.Context, id uuid.UUID, product *models.Product) (*models.Product, error) {
87110
ret := _m.Called(ctx, id, product)

internal/users/repository_mock.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ func (_m *MockUserRepository) Create(ctx context.Context, name string, email str
3636
return r0, r1
3737
}
3838

39-
// FindByEmail provides a mock function with given fields: ctx, email
4039
func (_m *MockUserRepository) FindByEmail(ctx context.Context, email string) (*models.User, error) {
4140
ret := _m.Called(ctx, email)
4241

@@ -59,7 +58,6 @@ func (_m *MockUserRepository) FindByEmail(ctx context.Context, email string) (*m
5958
return r0, r1
6059
}
6160

62-
// FindByID provides a mock function with given fields: ctx, id
6361
func (_m *MockUserRepository) FindByID(ctx context.Context, id uuid.UUID) (*models.User, error) {
6462
ret := _m.Called(ctx, id)
6563

main

15.1 MB
Binary file not shown.

0 commit comments

Comments
 (0)