-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
/
pagination.go
247 lines (216 loc) · 6.32 KB
/
pagination.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
//go:build go1.18
// +build go1.18
/*
Until go version 2, we can't really apply the type alias feature on a generic type or function,
so keep it separated on x/pagination.
import "github.com/kataras/iris/v12/context"
type ListResponse[T any] = context.ListResponse[T]
OR
type ListResponse = context.ListResponse doesn't work.
The only workable thing for generic aliases is when you know the type e.g.
type ListResponse = context.ListResponse[any] but that doesn't fit us.
*/
package pagination
import (
"math"
"net/http"
"strconv"
)
var (
// MaxSize defines the max size of items to display.
MaxSize = 100000
// DefaultSize defines the default size when ListOptions.Size is zero.
DefaultSize = MaxSize
)
// ListOptions is the list request object which should be provided by the client through
// URL Query. Then the server passes that options to a database query,
// including any custom filters may be given from the request body and,
// then the server responds back with a `Context.JSON(NewList(...))` response based
// on the database query's results.
type ListOptions struct {
// Current page number.
// If Page > 0 then:
// Limit = DefaultLimit
// Offset = DefaultLimit * Page
// If Page == 0 then no actual data is return,
// internally we must check for this value
// because in postgres LIMIT 0 returns the columns but with an empty set.
Page int `json:"page" url:"page"`
// The elements to get, this modifies the LIMIT clause,
// this Size can't be higher than the MaxSize.
// If Size is zero then size is set to DefaultSize.
Size int `json:"size" url:"size"`
}
// GetLimit returns the LIMIT value of a query.
func (opts ListOptions) GetLimit() int {
if opts.Size > 0 && opts.Size < MaxSize {
return opts.Size
}
return DefaultSize
}
// GetLimit returns the OFFSET value of a query.
func (opts ListOptions) GetOffset() int {
if opts.Page > 1 {
return (opts.Page - 1) * opts.GetLimit()
}
return 0
}
// GetCurrentPage returns the Page or 1.
func (opts ListOptions) GetCurrentPage() int {
current := opts.Page
if current == 0 {
current = 1
}
return current
}
// GetNextPage returns the next page, current page + 1.
func (opts ListOptions) GetNextPage() int {
return opts.GetCurrentPage() + 1
}
// Bind binds the ListOptions values to a request value.
// It should be used as an x/client.RequestOption to fire requests
// on a server that supports pagination.
func (opts ListOptions) Bind(r *http.Request) error {
page := strconv.Itoa(opts.GetCurrentPage())
size := strconv.Itoa(opts.GetLimit())
q := r.URL.Query()
q.Set("page", page)
q.Set("size", size)
return nil
}
// List is the http response of a server handler which should render
// items with pagination support.
type List[T any] struct {
CurrentPage int `json:"current_page"` // the current page.
PageSize int `json:"page_size"` // the total amount of the entities return.
TotalPages int `json:"total_pages"` // the total number of pages based on page, size and total count.
TotalItems int64 `json:"total_items"` // the total number of rows.
HasNextPage bool `json:"has_next_page"` // true if more data can be fetched, depending on the current page * page size and total pages.
Filter any `json:"filter"` // if any filter data.
Items []T `json:"items"` // Items is empty array if no objects returned. Do NOT modify from outside.
}
// NewList returns a new List response which holds
// the current page, page size, total pages, total items count, any custom filter
// and the items array.
//
// Example Code:
//
// import "github.com/kataras/iris/v12/x/pagination"
// ...more code
//
// type User struct {
// Firstname string `json:"firstname"`
// Lastname string `json:"lastname"`
// }
//
// type ExtraUser struct {
// User
// ExtraData string
// }
//
// func main() {
// users := []User{
// {"Gerasimos", "Maropoulos"},
// {"Efi", "Kwfidou"},
// }
//
// t := pagination.NewList(users, 100, nil, pagination.ListOptions{
// Page: 1,
// Size: 50,
// })
//
// // Optionally, transform a T list of objects to a V list of objects.
// v, err := pagination.TransformList(t, func(u User) (ExtraUser, error) {
// return ExtraUser{
// User: u,
// ExtraData: "test extra data",
// }, nil
// })
// if err != nil { panic(err) }
//
// paginationJSON, err := json.MarshalIndent(v, "", " ")
// if err!=nil { panic(err) }
// fmt.Println(paginationJSON)
// }
func NewList[T any](items []T, totalCount int64, filter any, opts ListOptions) *List[T] {
pageSize := opts.GetLimit()
n := len(items)
if n == 0 || pageSize <= 0 {
return &List[T]{
CurrentPage: 1,
PageSize: 0,
TotalItems: 0,
TotalPages: 0,
Filter: filter,
Items: make([]T, 0),
}
}
numberOfPages := int(roundUp(float64(totalCount)/float64(pageSize), 0))
if numberOfPages <= 0 {
numberOfPages = 1
}
var hasNextPage bool
currentPage := opts.GetCurrentPage()
if totalCount == 0 {
currentPage = 1
}
if n > 0 {
hasNextPage = currentPage < numberOfPages
}
return &List[T]{
CurrentPage: currentPage,
PageSize: n,
TotalPages: numberOfPages,
TotalItems: totalCount,
HasNextPage: hasNextPage,
Filter: filter,
Items: items,
}
}
// TransformList accepts a List response and converts to a list of V items.
// T => from
// V => to
//
// Example Code:
//
// listOfUsers := pagination.NewList(...)
// newListOfExtraUsers, err := pagination.TransformList(listOfUsers, func(u User) (ExtraUser, error) {
// return ExtraUser{
// User: u,
// ExtraData: "test extra data",
// }, nil
// })
func TransformList[T any, V any](list *List[T], transform func(T) (V, error)) (*List[V], error) {
if list == nil {
return &List[V]{
CurrentPage: 1,
PageSize: 0,
TotalItems: 0,
TotalPages: 0,
Filter: nil,
Items: make([]V, 0),
}, nil
}
items := list.Items
toItems := make([]V, 0, len(items))
for _, fromItem := range items {
toItem, err := transform(fromItem)
if err != nil {
return nil, err
}
toItems = append(toItems, toItem)
}
newList := &List[V]{
CurrentPage: list.CurrentPage,
PageSize: list.PageSize,
TotalItems: list.TotalItems,
TotalPages: list.TotalPages,
Filter: list.Filter,
Items: toItems,
}
return newList, nil
}
func roundUp(input float64, places float64) float64 {
pow := math.Pow(10, places)
return math.Ceil(pow*input) / pow
}