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  }