Adding pagination support to movies

main
Drew Bednar 4 days ago
parent 7062e86c5b
commit ea0138e827

@ -18,6 +18,15 @@ curl -i http://0.0.0.0:5002/v1/movies/1
curl -i -X POST -d '{"title":"Passengers","year":2016,"runtime":"120 mins", "genres":["comedy","adventure"]}' http://0.0.0.0:5002/v1/movies
```
```bash
curl -i -X POST -d '{"title":"Mortal Engines","year":2019,"runtime":"130 mins", "genres":["sci-fi","adventure"]}' http://0.0.0.0:5002/v1/movies
curl -i -X POST -d '{"title":"Everything Everywhere All at Once","year":2023,"runtime":"135 mins", "genres":["sci-fi","adventure"]}' http://0.0.0.0:5002/v1/movies
curl -i -X POST -d '{"title":"Renfield","year":2023,"runtime":"115 mins", "genres":["comedy","adventure"]}' http://0.0.0.0:5002/v1/movies
curl -i -X POST -d '{"title":"Mad Max Fury Road","year":2015,"runtime":"145 mins", "genres":["action","adventure", "sci-fi"]}' http://0.0.0.0:5002/v1/movies
curl -i -X POST -d '{"title":"Immortals","year":2011,"runtime":"135 mins", "genres":["action","adventure", "fantasy"]}' http://0.0.0.0:5002/v1/movies
curl -i -X POST -d '{"title":"Patlabor","year":1992,"runtime":"125 mins", "genres":["anime","sci-fi"]}' http://0.0.0.0:5002/v1/movies
```
```bash
curl -i -X POST -d '{"title":"The Batman","year":2022,"runtime":"177 mins", "genres":["action","adventure"]}' http://0.0.0.0:5002/v1/movies
```

@ -269,12 +269,12 @@ func (app *application) listMoviesHandler(w http.ResponseWriter, r *http.Request
return
}
movies, err := app.models.Movies.GetAll(r.Context(), input.Title, input.Genres, input.Filters)
movies, metadata, err := app.models.Movies.GetAll(r.Context(), input.Title, input.Genres, input.Filters)
if err != nil {
app.serverErrorResponse(w, r, err)
return
}
app.writeJSON(w, 200, envelope{"movies": movies}, nil)
app.writeJSON(w, 200, envelope{"movies": movies, "metadata": metadata}, nil)
}
func (app *application) healthCheckHandler(w http.ResponseWriter, r *http.Request) {

@ -41,3 +41,38 @@ func (f Filters) sortDirection() string {
}
return "ASC"
}
// limit returns the page_size provided as a query param
func (f Filters) limit() int {
return f.PageSize
}
// offset returns the offset of a pagination query based on
// client's page and page_size query params used.
func (f Filters) offset() int {
return (f.Page - 1) * f.PageSize
}
type Metadata struct {
CurrentPage int `json:"current_page,omitzero"`
PageSize int `json:"page_size,omitzero"`
FirstPage int `json:"first_page,omitzero"`
LastPage int `json:"last_page,omitzero"`
TotalRecords int `json:"total_records,omitzero"`
}
// calculateMetadata returns a Metadata struct from query results and
// query params used in a request
func calculateMetadata(totalRecords, page, pageSize int) Metadata {
if totalRecords == 0 {
return Metadata{}
}
return Metadata{
CurrentPage: page,
PageSize: pageSize,
FirstPage: 1,
LastPage: (totalRecords + pageSize - 1) / pageSize,
TotalRecords: totalRecords,
}
}

@ -172,7 +172,7 @@ func (m MovieModel) Delete(ctx context.Context, id int64) (err error) {
return nil
}
func (m MovieModel) GetAll(ctx context.Context, title string, genres []string, filters Filters) ([]*Movie, error) {
func (m MovieModel) GetAll(ctx context.Context, title string, genres []string, filters Filters) ([]*Movie, Metadata, error) {
// OLD query
// query := `
// SELECT id, created_at, title, year, runtime, genres, version
@ -199,12 +199,15 @@ func (m MovieModel) GetAll(ctx context.Context, title string, genres []string, f
// AND (genres @> $2 OR $2 = '{}')
// ORDER BY id`
// Using a window function to produce a totalRecords count using the WHERE parameters of
// the query
query := fmt.Sprintf(`
SELECT id, created_at, title, year, runtime, genres, version
SELECT count(*) OVER(), id, created_at, title, year, runtime, genres, version
FROM movies
WHERE (to_tsvector('simple', title) @@ plainto_tsquery('simple', $1) OR $1 = '')
AND (genres @> $2 OR $2 = '{}')
ORDER BY %s %s`, filters.sortColumn(), filters.sortDirection())
ORDER BY %s %s, id ASC
LIMIT $3 OFFSET $4`, filters.sortColumn(), filters.sortDirection())
// ctx want some timeout for queries. When used in the handler the context passed should
// be the r.Context. Since cancel functions are inherited it will cancel on client
@ -214,17 +217,21 @@ func (m MovieModel) GetAll(ctx context.Context, title string, genres []string, f
m.logger.Debug(query)
rows, err := m.db.Query(ctxTimeout, query, title, genres)
args := []any{title, genres, filters.limit(), filters.offset()}
rows, err := m.db.Query(ctxTimeout, query, args...)
if err != nil {
return nil, err
return nil, Metadata{}, err
}
defer rows.Close()
totalRecords := 0
movies := []*Movie{}
for rows.Next() {
var movie Movie
err := rows.Scan(
&totalRecords,
&movie.ID,
&movie.CreatedAt,
&movie.Title,
@ -234,16 +241,18 @@ func (m MovieModel) GetAll(ctx context.Context, title string, genres []string, f
&movie.Version,
)
if err != nil {
return nil, err
return nil, Metadata{}, err
}
movies = append(movies, &movie)
}
err = rows.Err()
if err != nil {
return nil, err
return nil, Metadata{}, err
}
return movies, nil
metadata := calculateMetadata(totalRecords, filters.Page, filters.PageSize)
return movies, metadata, nil
}
func ValidateMovie(v *validator.Validator, m *Movie) {

Loading…
Cancel
Save