Updating movie
continuous-integration/drone/push Build is failing Details

main
Drew Bednar 6 days ago
parent d241d21d70
commit 2936ce2439

@ -46,6 +46,10 @@ start-local:
UID=$$(id -u) GID=$$(id -g) docker compose up -d
.PHONY: start-local
db-local:
docker exec -it pulleydb psql postgres://pulley:passwd@localhost:5432/pulley
.PHONY: db-local
stop-local:
docker compose down
.PHONY: stop-local

@ -7,4 +7,15 @@
## Creating a movie
curl -i -X POST -d '{"title":"Moana","year":2016,"runtime":107, "genres":["animation","adventure"]}' http://0.0.0.0:5002/v1/movies
```bash
curl -i -X POST -d '{"title":"Moana","year":2016,"runtime":"107 mins", "genres":["animation","adventure"]}' 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
```
```bash
curl -i -X POST -d '{"title":"Death of a Unicorn","year":2025,"runtime":"126 mins", "genres":["comedy","satire"]}' http://0.0.0.0:5002/v1/movies
```

@ -1,14 +1,12 @@
package main
import (
"errors"
"fmt"
"net/http"
"strconv"
"time"
"git.runcible.io/learning/pulley/internal/data"
"git.runcible.io/learning/pulley/internal/validator"
"github.com/julienschmidt/httprouter"
)
func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Request) {
@ -49,13 +47,23 @@ func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Reques
app.failedValidationResponse(w, r, v.Errors)
}
// TODO save to DB
// We can implement a timeout context if we would like here
ctx := r.Context()
// Dump the contents of the input struct in a HTTP response
// +v is the default format value plus field names for structs
w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, "%+v\n", input)
err = app.models.Movies.Insert(ctx, m)
if err != nil {
app.serverErrorResponse(w, r, err)
}
// Include location headers in HTTP response for newly created resource
headers := make(http.Header)
headers.Set("Location", fmt.Sprintf("/v1/movies/%d", m.ID))
// Write a JSON 201 Response with movie data in the response body and Location header
err = app.writeJSON(w, http.StatusCreated, envelope{"movie": m}, headers)
if err != nil {
app.serverErrorResponse(w, r, err)
}
}
func (app *application) getMovieHandler(w http.ResponseWriter, r *http.Request) {
@ -64,25 +72,28 @@ func (app *application) getMovieHandler(w http.ResponseWriter, r *http.Request)
// retrieve a slice containing these parameter names and values.
// TODO refactor id retrieval to an app.readIDParam receiver
params := httprouter.ParamsFromContext(r.Context())
// params := httprouter.ParamsFromContext(r.Context())
// id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
id, err := app.readIDParam(r)
if err != nil || id < 1 {
// http.NotFound(w, r)
if err != nil {
app.notFoundResponse(w, r)
return
}
movie := data.Movie{
ID: id,
CreatedAt: time.Now(),
Title: "Eisenhorn",
// Runtime: 102,
Genres: []string{"sci-fi", "action"},
Version: 1,
movie, err := app.models.Movies.Get(r.Context(), id)
if err != nil {
switch {
case errors.Is(err, data.ErrRecordNotFound):
app.notFoundResponse(w, r)
return
default:
app.serverErrorResponse(w, r, err)
return
}
}
app.logger.Info("Hit the get movies and found", "movie", movie.Title)
err = app.writeJSON(w, http.StatusOK, envelope{"movie": movie}, nil)
if err != nil {
@ -92,6 +103,62 @@ func (app *application) getMovieHandler(w http.ResponseWriter, r *http.Request)
}
}
func (app *application) updateMovieHandler(w http.ResponseWriter, r *http.Request) {
id, err := app.readIDParam(r)
if err != nil {
app.notFoundResponse(w, r)
return
}
movie, err := app.models.Movies.Get(r.Context(), id)
if err != nil {
switch {
case errors.Is(err, data.ErrRecordNotFound):
app.notFoundResponse(w, r)
default:
app.serverErrorResponse(w, r, err)
}
return
}
var input struct {
Title string `json:"title"`
Year int32 `json:"year"`
Runtime data.Runtime `json:"runtime"`
Genres []string `json:"genres"`
}
err = app.readJSON(w, r, &input)
if err != nil {
app.badRequestResponse(w, r, err)
return
}
movie.Title = input.Title
movie.Year = input.Year
movie.Runtime = input.Runtime
movie.Genres = input.Genres
v := validator.New()
if validator.ValidateMovie(v, movie); !v.Valid() {
app.failedValidationResponse(w, r, v.Errors)
return
}
err = app.models.Movies.Update(r.Context(), movie)
if err != nil {
app.serverErrorResponse(w, r, err)
return
}
err = app.writeJSON(w, http.StatusOK, envelope{"movie": movie}, nil)
if err != nil {
app.serverErrorResponse(w, r, err)
}
}
func (app *application) healthCheckHandler(w http.ResponseWriter, r *http.Request) {
// JSON has to be double quoted. FYI you would never really do this
// js := `{"status": "available", "environment": %q, "version": %q}`

@ -10,7 +10,10 @@ import (
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/julienschmidt/httprouter"
)
type envelope map[string]any
@ -142,3 +145,14 @@ func (app *application) readJSON(w http.ResponseWriter, r *http.Request, dst any
return nil
}
func (a *application) readIDParam(r *http.Request) (int64, error) {
params := httprouter.ParamsFromContext(r.Context())
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if id < 1 {
return id, fmt.Errorf("integer less than 1")
}
return id, err
}

@ -16,6 +16,7 @@ import (
"time"
"git.runcible.io/learning/pulley/internal/config"
"git.runcible.io/learning/pulley/internal/data"
"git.runcible.io/learning/pulley/internal/database"
"git.runcible.io/learning/pulley/internal/logging"
"git.runcible.io/learning/pulley/migrations"
@ -32,6 +33,7 @@ const Version = "1.0.0"
type application struct {
config config.ServiceConfig
logger *slog.Logger
models data.Models
}
func run(ctx context.Context, w io.Writer, args []string) error {
@ -90,7 +92,9 @@ func run(ctx context.Context, w io.Writer, args []string) error {
pool.Close()
}()
app := application{config: cfg, logger: logger}
models := data.NewModels(pool)
app := application{config: cfg, logger: logger, models: models}
srv := &http.Server{
Addr: fmt.Sprintf("%s:%d", "0.0.0.0", app.config.Port),

@ -25,6 +25,7 @@ func (app *application) routes() http.Handler {
router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthCheckHandler)
router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler)
router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.getMovieHandler)
router.HandlerFunc(http.MethodPut, "/v1/movies/:id", app.updateMovieHandler)
// middleware
return app.recoverPanic(router)

@ -0,0 +1,22 @@
package data
import (
"errors"
"github.com/jackc/pgx/v5/pgxpool"
)
var (
ErrRecordNotFound = errors.New("record not found")
)
// Models is a wrapper around all model repository objects
type Models struct {
Movies MovieModel
}
func NewModels(pool *pgxpool.Pool) Models {
return Models{
Movies: MovieModel{pool: pool},
}
}

@ -1,6 +1,13 @@
package data
import "time"
import (
"context"
"errors"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// MUST export fields to serialize them
@ -19,3 +26,78 @@ type Movie struct {
Genres []string `json:"genres,omitzero"`
Version int32 `json:"version"`
}
type MovieModel struct {
pool *pgxpool.Pool
}
func (m MovieModel) Insert(ctx context.Context, movie *Movie) error {
query := `
INSERT INTO movies (title, year, runtime, genres)
VALUES ($1, $2, $3, $4)
RETURNING id, created_at, version`
args := []any{movie.Title, movie.Year, movie.Runtime, movie.Genres}
row := m.pool.QueryRow(ctx, query, args...)
// Insert is mutating the Movie struct
err := row.Scan(&movie.ID, &movie.CreatedAt, &movie.Version)
return err
}
func (m MovieModel) Get(ctx context.Context, id int64) (*Movie, error) {
// safety validation
if id < 1 {
return nil, ErrRecordNotFound
}
query := `
SELECT id, created_at, title, year, runtime, genres, version
FROM movies
WHERE id = $1
`
var movie Movie
err := m.pool.QueryRow(ctx, query, id).Scan(
&movie.ID,
&movie.CreatedAt,
&movie.Title,
&movie.Year,
&movie.Runtime,
&movie.Genres,
&movie.Version,
)
if err != nil {
switch {
case errors.Is(err, pgx.ErrNoRows):
return nil, ErrRecordNotFound
default:
return nil, err
}
}
return &movie, nil
}
func (m MovieModel) Update(ctx context.Context, movie *Movie) error {
query := `
UPDATE movies
SET title = $1, year = $2, runtime = $3, genres = $4, version = version + 1
WHERE id = $5
RETURNING version
`
args := []any{
movie.Title,
movie.Year,
movie.Runtime,
movie.Genres,
movie.ID,
}
return m.pool.QueryRow(ctx, query, args...).Scan(&movie.Version)
}
func (m MovieModel) Delete(ctx context.Context, id int64) error {
return nil
}

Loading…
Cancel
Save