From 17267ed14553bf425f1df4d8d40c9d2cffcaefff Mon Sep 17 00:00:00 2001 From: lbenedar Date: Wed, 18 Mar 2026 16:31:33 +0300 Subject: [PATCH] ch9.2-9.3 --- cmd/api/helpers.go | 37 ++++++++++++++++++++++++++++++++ cmd/api/movies.go | 44 ++++++++++++++++++++++++++++++++++++++ cmd/api/routes.go | 2 ++ internal/data/filters.go | 19 +++++++++++++++++ internal/data/models.go | 1 + internal/data/movies.go | 46 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 149 insertions(+) create mode 100644 internal/data/filters.go diff --git a/cmd/api/helpers.go b/cmd/api/helpers.go index 4848e90..a39dcb2 100644 --- a/cmd/api/helpers.go +++ b/cmd/api/helpers.go @@ -6,9 +6,11 @@ import ( "fmt" "io" "net/http" + "net/url" "strconv" "strings" + "gitea.local.lab/Lbenedar/greenlight/internal/validator" "github.com/julienschmidt/httprouter" ) @@ -83,3 +85,38 @@ func (app *application) readJSON(w http.ResponseWriter, r *http.Request, dst any } return nil } + +func (app *application) readString(qs url.Values, key string, defaultValue string) string { + s := qs.Get(key) + + if s == "" { + return defaultValue + } + + return s +} + +func (app *application) readCSV(qs url.Values, key string, defaultValue []string) []string { + csv := qs.Get(key) + + if csv == "" { + return defaultValue + } + + return strings.Split(csv, ",") +} + +func (app *application) readInt(qs url.Values, key string, defaultValue int, v *validator.Validator) int { + s := qs.Get(key) + + if s == "" { + return defaultValue + } + + i, err := strconv.Atoi(s) + if err != nil { + v.AddError(key, "must be an integer value") + return defaultValue + } + return i +} diff --git a/cmd/api/movies.go b/cmd/api/movies.go index 662bc79..d06c89a 100644 --- a/cmd/api/movies.go +++ b/cmd/api/movies.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/http" + "strconv" "gitea.local.lab/Lbenedar/greenlight/internal/data" "gitea.local.lab/Lbenedar/greenlight/internal/validator" @@ -93,6 +94,13 @@ func (app *application) updateMovieHandler(w http.ResponseWriter, r *http.Reques return } + if r.Header.Get("X-Expected-Version") != "" { + if strconv.FormatInt(int64(movie.Version), 32) != r.Header.Get("X-Expected-Version") { + app.editConflictResponse(w, r) + return + } + } + var input struct { Title *string `json:"title"` Year *int32 `json:"year"` @@ -165,3 +173,39 @@ func (app *application) deleteMovieHandler(w http.ResponseWriter, r *http.Reques app.serverErrorResponse(w, r, err) } } + +func (app *application) listMoviesHandler(w http.ResponseWriter, r *http.Request) { + var input struct { + Title string + Genres []string + data.Filters + } + + v := validator.New() + + qs := r.URL.Query() + + input.Title = app.readString(qs, "title", "") + input.Genres = app.readCSV(qs, "genres", []string{}) + input.Filters.Page = app.readInt(qs, "page", 1, v) + input.Filters.PageSize = app.readInt(qs, "page_size", 20, v) + input.Filters.Sort = app.readString(qs, "sort", "id") + + input.Filters.SortSafelist = []string{"id", "title", "year", "runtime", "-id", "-title", "-year", "-runtime"} + + if data.ValidateFilters(v, input.Filters); !v.Valid() { + app.failedValidationResponse(w, r, v.Errors) + return + } + + movies, err := app.models.Movies.GetAll(input.Title, input.Genres, input.Filters) + if err != nil { + app.serverErrorResponse(w, r, err) + return + } + + err = app.writeJSON(w, http.StatusOK, envelope{"movies": movies}, nil) + if err != nil { + app.serverErrorResponse(w, r, err) + } +} diff --git a/cmd/api/routes.go b/cmd/api/routes.go index 5b1b003..5acf321 100644 --- a/cmd/api/routes.go +++ b/cmd/api/routes.go @@ -13,6 +13,8 @@ func (app *application) routes() *httprouter.Router { router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse) router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler) + + router.HandlerFunc(http.MethodGet, "/v1/movies", app.listMoviesHandler) router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler) router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.showMovieHandler) router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.updateMovieHandler) diff --git a/internal/data/filters.go b/internal/data/filters.go new file mode 100644 index 0000000..d92e738 --- /dev/null +++ b/internal/data/filters.go @@ -0,0 +1,19 @@ +package data + +import "gitea.local.lab/Lbenedar/greenlight/internal/validator" + +type Filters struct { + Page int + PageSize int + Sort string + SortSafelist []string +} + +func ValidateFilters(v *validator.Validator, f Filters) { + v.Check(f.Page > 0, "page", "must be greater than zero") + v.Check(f.Page <= 10_000_000, "page", "must be a maximum of 10 million") + v.Check(f.PageSize > 0, "page_size", "must be greater than zero") + v.Check(f.PageSize <= 100, "page_size", "must be a maximum of 100") + + v.Check(validator.PermittedValue(f.Sort, f.SortSafelist...), "sort", "invalid sort values") +} diff --git a/internal/data/models.go b/internal/data/models.go index 6106037..e4a539f 100644 --- a/internal/data/models.go +++ b/internal/data/models.go @@ -16,6 +16,7 @@ type Models struct { Get(id int64) (*Movie, error) Update(movie *Movie) error Delete(id int64) error + GetAll(title string, genres []string, filter Filters) ([]*Movie, error) } } diff --git a/internal/data/movies.go b/internal/data/movies.go index a6a8c4d..9321e97 100644 --- a/internal/data/movies.go +++ b/internal/data/movies.go @@ -134,6 +134,48 @@ func (m MovieModel) Delete(id int64) error { return nil } +func (m MovieModel) GetAll(title string, genres []string, filter Filters) ([]*Movie, error) { + query := ` + SELECT id, created_at, title, year, runtime, genres, version + FROM movies + ORDER BY id` + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + rows, err := m.DB.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + movies := []*Movie{} + + for rows.Next() { + var movie Movie + + err := rows.Scan( + &movie.ID, + &movie.CreatedAt, + &movie.Title, + &movie.Year, + &movie.Runtime, + pq.Array(&movie.Genres), + &movie.Version, + ) + if err != nil { + return nil, err + } + movies = append(movies, &movie) + } + + if err = rows.Err(); err != nil { + return nil, err + } + + return movies, nil +} + type MockMovieModel struct{} func (m MockMovieModel) Insert(movie *Movie) error { @@ -152,6 +194,10 @@ func (m MockMovieModel) Delete(id int64) error { return nil } +func (m MockMovieModel) GetAll(title string, genres []string, filter Filters) ([]*Movie, error) { + return nil, nil +} + func ValidateMovie(v *validator.Validator, movie *Movie) { v.Check(movie.Title != "", "title", "must be provided") v.Check(len(movie.Title) <= 500, "title", "must not be more than 500 bytes long")