gitlab.com/ignitionrobotics/web/ign-go@v1.0.0-rc4/pagination.go (about) 1 package ign 2 3 import ( 4 "fmt" 5 "github.com/jinzhu/gorm" 6 "net/http" 7 "net/url" 8 "strconv" 9 ) 10 11 const ( 12 defaultPageSize = 20 13 maxPageSize = 100 14 defaultPageNumber = 1 15 16 pageArgName = "page" 17 perPageArgName = "per_page" 18 ) 19 20 ////////////////////////////////////// 21 22 // Pagination module is used to perform GORM 'Find' queries in 23 // a paginated way. 24 // The typical usage is the following: 25 // 1) Create a PaginationRequest from que HTTP request. This means 26 // reading 'page' and 'per_page' arguments sent by the user in the 27 // URL query. 28 // eg. pagRequest := NewPaginationRequest(r) 29 // 2) Create your GORM Query and the paginate it: 30 // eg. q := db.Model(&Model{}) 31 // pagResult := PaginateQuery(q, result, pagRequest) 32 // 3) Write the prev and next headers in the output response 33 // WritePaginationHeaders(pagResult, w, r) 34 35 ////////////////////////////////////// 36 37 // PaginationRequest represents the pagination values requested 38 // in the URL query (eg. ?page=2&per_page=10) 39 type PaginationRequest struct { 40 // Flag that indicates if the request included a "page" argument. 41 PageRequested bool 42 // The requested page number (value >= 1) 43 Page int64 44 // The requested number of items per page. 45 PerPage int64 46 // The original request URL 47 URL string 48 } 49 50 // NewPaginationRequest creates a new PaginationRequest from the given http request. 51 func NewPaginationRequest(r *http.Request) (*PaginationRequest, *ErrMsg) { 52 pageRequest := PaginationRequest{ 53 PageRequested: false, 54 Page: defaultPageNumber, 55 PerPage: defaultPageSize, 56 URL: r.URL.String(), 57 } 58 var err error 59 60 // Parse request arguments 61 62 // Process "page" argument 63 pageStr := r.URL.Query().Get(pageArgName) 64 if pageStr != "" { 65 pageRequest.PageRequested = true 66 pageRequest.Page, err = strconv.ParseInt(pageStr, 10, 64) 67 if err != nil { 68 return nil, NewErrorMessageWithArgs(ErrorInvalidPaginationRequest, err, []string{pageArgName}) 69 } 70 if pageRequest.Page <= 0 { 71 return nil, NewErrorMessageWithArgs(ErrorInvalidPaginationRequest, nil, []string{pageArgName}) 72 } 73 } 74 75 // Process "per_page" argument 76 perPageStr := r.URL.Query().Get(perPageArgName) 77 if perPageStr != "" { 78 pageRequest.PerPage, err = strconv.ParseInt(perPageStr, 10, 64) 79 if err != nil { 80 return nil, NewErrorMessageWithArgs(ErrorInvalidPaginationRequest, err, []string{perPageArgName}) 81 } 82 if pageRequest.PerPage <= 0 { 83 return nil, NewErrorMessageWithArgs(ErrorInvalidPaginationRequest, err, []string{perPageArgName}) 84 } 85 if pageRequest.PerPage > maxPageSize { 86 pageRequest.PerPage = defaultPageSize 87 } 88 } 89 return &pageRequest, nil 90 } 91 92 ////////////////////////////////////// 93 94 // PaginationResult represents the actual pagination output. 95 type PaginationResult struct { 96 // Page number 97 Page int64 98 // Page size 99 PerPage int64 100 // Original request' url 101 URL string 102 // Query "total" count (ie. this is NOT the "page" count) 103 QueryCount int64 104 // A page is considered "found" if it is within the range of valid pages, 105 // OR if it is the first page and the DB query is empty. In this empty scenario, 106 // we want to return status OK with zero elements, rather than a 404 status. 107 PageFound bool 108 } 109 110 func newPaginationResult() PaginationResult { 111 return PaginationResult{} 112 } 113 114 ////////////////////////////////////// 115 116 func computeLastPage(page *PaginationResult) int64 { 117 mod := page.QueryCount % page.PerPage 118 lastPage := page.QueryCount / page.PerPage 119 if mod > 0 { 120 lastPage++ 121 } 122 return lastPage 123 } 124 125 // PaginateQuery applies a pagination request to a GORM query and executes it. 126 // Param[in] q [gorm.DB] The query to be paginated 127 // Param[out] result [interface{}] The paginated list of items 128 // Param[in] p The pagination request 129 // Returns a PaginationResult describing the returned page. 130 func PaginateQuery(q *gorm.DB, result interface{}, p PaginationRequest) (*PaginationResult, error) { 131 q = q.Limit(int(p.PerPage)) 132 q = q.Offset((Max(p.Page, 1) - 1) * p.PerPage) 133 q = q.Find(result) 134 if err := q.Error; err != nil { 135 return nil, err 136 } 137 q = q.Limit(-1) 138 q = q.Offset(-1) 139 count := 0 140 if err := q.Count(&count).Error; err != nil { 141 return nil, err 142 } 143 144 r := newPaginationResult() 145 r.Page = p.Page 146 r.PerPage = p.PerPage 147 r.URL = p.URL 148 r.QueryCount = int64(count) 149 150 lastPage := computeLastPage(&r) 151 // A page is considered "found" if it is within the range of valid pages, 152 // OR if it is the first page and the DB query is empty. In this empty scenario, 153 // we want to return status OK with zero elements, rather than a 404 status. 154 r.PageFound = r.Page <= lastPage || (r.Page == 1 && r.QueryCount == 0) 155 156 return &r, nil 157 } 158 159 ////////////////////////////////////// 160 161 // newLinkStr is a helper function to create a page link header string. 162 func newLinkStr(u *url.URL, page int64, name string) string { 163 params := u.Query() 164 params.Set(pageArgName, fmt.Sprint(page)) 165 u.RawQuery = params.Encode() 166 return fmt.Sprintf("<%s>; rel=\"%s\"", u, name) 167 } 168 169 // WritePaginationHeaders writes the 'next', 'last', 'first', and 'prev' Link headers to the given 170 // ResponseWriter. 171 func WritePaginationHeaders(page PaginationResult, w http.ResponseWriter, r *http.Request) error { 172 u, _ := url.Parse(page.URL) 173 params := u.Query() 174 params.Set(perPageArgName, fmt.Sprint(page.PerPage)) 175 176 lastPage := computeLastPage(&page) 177 178 var links []string 179 180 // Next and Last 181 if page.Page < lastPage { 182 links = append(links, newLinkStr(u, page.Page+1, "next")) 183 links = append(links, newLinkStr(u, lastPage, "last")) 184 } 185 186 // First and Prev 187 if page.Page > 1 { 188 links = append(links, newLinkStr(u, 1, "first")) 189 prev := page.Page - 1 190 if page.Page > lastPage { 191 prev = lastPage 192 } 193 links = append(links, newLinkStr(u, prev, "prev")) 194 } 195 196 // Build the output Links header 197 c := len(links) 198 headerStr := "" 199 for i, l := range links { 200 headerStr += l 201 if i+1 < c { 202 headerStr += ", " 203 } 204 } 205 if headerStr != "" { 206 w.Header().Set("Link", headerStr) 207 } 208 w.Header().Set("X-Total-Count", fmt.Sprint(page.QueryCount)) 209 return nil 210 }