diff --git a/NOTES.md b/NOTES.md index 75a205e..1c53ff9 100644 --- a/NOTES.md +++ b/NOTES.md @@ -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 ``` diff --git a/cmd/api/handlers.go b/cmd/api/handlers.go index 416eefe..7a7176f 100644 --- a/cmd/api/handlers.go +++ b/cmd/api/handlers.go @@ -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) { diff --git a/internal/data/filters.go b/internal/data/filters.go index 58ceee0..a53dc52 100644 --- a/internal/data/filters.go +++ b/internal/data/filters.go @@ -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, + } +} diff --git a/internal/data/movies.go b/internal/data/movies.go index 90e130d..f692abc 100644 --- a/internal/data/movies.go +++ b/internal/data/movies.go @@ -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) {