Skip to content

Commit 1f60255

Browse files
authored
Merge pull request #4 from Caknoooo/task/restructuring-clean-explanation
Task/restructuring clean explanation
2 parents 12bb778 + 35b7a02 commit 1f60255

7 files changed

Lines changed: 155 additions & 1 deletion

File tree

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,33 @@ curl "http://localhost:8080/users?search=admin&sort=id,desc&page=2&per_page=5"
9191

9292
## 🗂️ Advanced Filtering
9393

94-
### Custom Filter Pattern with Validation
94+
Use this constructor in every implemented builder.
95+
```go
96+
type XQuery struct {
97+
db *gorm.DB
98+
}
9599

100+
func NewXQuery(db *gorm.DB) *XQuery {
101+
return &XQuery{
102+
db: db,
103+
}
104+
}
105+
```
106+
107+
### Custom Filter Pattern with Validation
96108
Create powerful, reusable filters with automatic validation:
97109

98110
```go
111+
type UserQuery struct {
112+
db *gorm.DB
113+
}
114+
115+
func NewUserQuery(db *gorm.DB) *UserQuery {
116+
return &UserQuery{
117+
db: db,
118+
}
119+
}
120+
99121
type UserFilter struct {
100122
pagination.BaseFilter
101123
ID int `json:"id" form:"id"`
@@ -190,6 +212,16 @@ curl "http://localhost:8080/users?role=user&is_active=true&min_age=25&search=dev
190212
### Basic Relationship Loading with Security
191213

192214
```go
215+
type UserQuery struct {
216+
db *gorm.DB
217+
}
218+
219+
func NewUserQuery(db *gorm.DB) *UserQuery {
220+
return &UserQuery{
221+
db: db,
222+
}
223+
}
224+
193225
type User struct {
194226
ID uint `json:"id" gorm:"primaryKey"`
195227
Name string `json:"name"`

examples/athlete.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ import (
55
"gorm.io/gorm"
66
)
77

8+
type AthleteQuery struct {
9+
db *gorm.DB
10+
}
11+
12+
func NewAthleteQuery(db *gorm.DB) *AthleteQuery {
13+
return &AthleteQuery{
14+
db: db,
15+
}
16+
}
17+
818
type Athlete struct {
919
ID int `json:"id"`
1020
ProvinceID int `json:"province_id"`

examples/event.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ import (
77
"gorm.io/gorm"
88
)
99

10+
type EventQuery struct {
11+
db *gorm.DB
12+
}
13+
14+
func NewEventQuery(db *gorm.DB) *EventQuery {
15+
return &EventQuery{
16+
db: db,
17+
}
18+
}
19+
1020
type Event struct {
1121
ID uint `json:"id" gorm:"primaryKey"`
1222
Name string `json:"name" gorm:"column:name"`

examples/province.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ import (
55
"gorm.io/gorm"
66
)
77

8+
type ProvinceQuery struct {
9+
db *gorm.DB
10+
}
11+
12+
func NewProvinceQuery(db *gorm.DB) *ProvinceQuery {
13+
return &ProvinceQuery{
14+
db: db,
15+
}
16+
}
17+
818
type Province struct {
919
ID uint `json:"id" gorm:"primaryKey"`
1020
Name string `json:"name" gorm:"column:name"`

examples/sport.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ import (
55
"gorm.io/gorm"
66
)
77

8+
type SportQuery struct {
9+
db *gorm.DB
10+
}
11+
12+
func NewSportQuery(db *gorm.DB) *SportQuery {
13+
return &SportQuery{
14+
db: db,
15+
}
16+
}
17+
818
type Sport struct {
919
ID uint `json:"id" gorm:"primaryKey"`
1020
Name string `json:"name" gorm:"column:name"`

helpers.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,65 @@ func PaginatedAPIResponseWithIncludes[T any](
197197

198198
return NewPaginatedResponse(200, message, data, paginationResponse)
199199
}
200+
201+
// PaginatedQueryWithQueryLayer provides pagination using query layer pattern
202+
// This function separates the database logic from the handler
203+
func PaginatedQueryWithQueryLayer[T any](
204+
filter IncludableQueryBuilder,
205+
queryFunc func(IncludableQueryBuilder) ([]T, int64, error),
206+
) ([]T, int64, error) {
207+
// Validate includes before processing
208+
if validator, ok := filter.(interface{ Validate() }); ok {
209+
validator.Validate()
210+
}
211+
212+
// Execute query through query layer
213+
return queryFunc(filter)
214+
}
215+
216+
// PaginatedAPIResponseWithQueryLayer creates a complete API response using query layer pattern
217+
func PaginatedAPIResponseWithQueryLayer[T any](
218+
ctx *gin.Context,
219+
filter IncludableQueryBuilder,
220+
message string,
221+
queryFunc func(IncludableQueryBuilder) ([]T, int64, error),
222+
) PaginatedResponse {
223+
// Bind pagination from context
224+
if baseFilter, ok := filter.(interface{ BindPagination(*gin.Context) }); ok {
225+
baseFilter.BindPagination(ctx)
226+
}
227+
228+
// Bind custom filter parameters
229+
if err := ctx.ShouldBindQuery(filter); err != nil {
230+
return NewPaginatedResponse(400, "Invalid query parameters: "+err.Error(), nil, PaginationResponse{})
231+
}
232+
233+
// Execute query through query layer
234+
data, total, err := PaginatedQueryWithQueryLayer(filter, queryFunc)
235+
if err != nil {
236+
return NewPaginatedResponse(500, "Internal Server Error: "+err.Error(), nil, PaginationResponse{})
237+
}
238+
239+
paginationResponse := CalculatePagination(filter.GetPagination(), total)
240+
return NewPaginatedResponse(200, message, data, paginationResponse)
241+
}
242+
243+
// BindAndValidateFilter binds pagination and query parameters, then validates the filter
244+
func BindAndValidateFilter(ctx *gin.Context, filter IncludableQueryBuilder) error {
245+
// Bind pagination from context
246+
if baseFilter, ok := filter.(interface{ BindPagination(*gin.Context) }); ok {
247+
baseFilter.BindPagination(ctx)
248+
}
249+
250+
// Bind custom filter parameters
251+
if err := ctx.ShouldBindQuery(filter); err != nil {
252+
return err
253+
}
254+
255+
// Validate includes
256+
if validator, ok := filter.(interface{ Validate() }); ok {
257+
validator.Validate()
258+
}
259+
260+
return nil
261+
}

query_builder.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ type AllowedIncludesProvider interface {
2525
GetAllowedIncludes() map[string]bool
2626
}
2727

28+
// DatabaseProvider interface for query builders that need database access
29+
type DatabaseProvider interface {
30+
GetDB() *gorm.DB
31+
}
32+
33+
// QueryLayerBuilder interface that combines query building with database access
34+
type QueryLayerBuilder interface {
35+
IncludableQueryBuilder
36+
DatabaseProvider
37+
}
38+
2839
// applyAutoSearch applies search automatically based on provided search fields
2940
func applyAutoSearch(query *gorm.DB, searchTerm string, searchFields []string, dialect DatabaseDialect) *gorm.DB {
3041
if len(searchFields) == 0 || searchTerm == "" {
@@ -94,6 +105,15 @@ func PaginatedQueryWithIncludable[T any](
94105
db *gorm.DB,
95106
builder IncludableQueryBuilder,
96107
) ([]T, int64, error) {
108+
// If db is nil, try to get it from the builder (for query layer pattern)
109+
if db == nil {
110+
if dbProvider, ok := builder.(DatabaseProvider); ok {
111+
db = dbProvider.GetDB()
112+
} else {
113+
return nil, 0, fmt.Errorf("database connection not provided")
114+
}
115+
}
116+
97117
// Validate the builder
98118
builder.Validate()
99119

0 commit comments

Comments
 (0)