put and delete
This commit is contained in:
parent
ca0abff155
commit
d9b39dbbb9
62
app/movie.go
62
app/movie.go
|
@ -9,25 +9,16 @@ import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"ewintr.nl/emdb/movie"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Movie struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Year int `json:"year"`
|
|
||||||
IMDBID string `json:"imdb_id"`
|
|
||||||
WatchedOn string `json:"watched_on"`
|
|
||||||
Rating int `json:"rating"`
|
|
||||||
Comment string `json:"comment"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MovieAPI struct {
|
type MovieAPI struct {
|
||||||
repo *SQLite
|
repo movie.MovieRepository
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMovieAPI(repo *SQLite, logger *slog.Logger) *MovieAPI {
|
func NewMovieAPI(repo movie.MovieRepository, logger *slog.Logger) *MovieAPI {
|
||||||
return &MovieAPI{
|
return &MovieAPI{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
logger: logger.With("api", "movie"),
|
logger: logger.With("api", "movie"),
|
||||||
|
@ -37,16 +28,20 @@ func NewMovieAPI(repo *SQLite, logger *slog.Logger) *MovieAPI {
|
||||||
func (api *MovieAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (api *MovieAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
logger := api.logger.With("method", "serveHTTP")
|
logger := api.logger.With("method", "serveHTTP")
|
||||||
|
|
||||||
movieID, _ := ShiftPath(r.URL.Path)
|
subPath, _ := ShiftPath(r.URL.Path)
|
||||||
switch {
|
switch {
|
||||||
case r.Method == http.MethodGet && movieID != "":
|
case r.Method == http.MethodGet && subPath != "":
|
||||||
api.Read(w, r, movieID)
|
api.Read(w, r, subPath)
|
||||||
case r.Method == http.MethodGet && movieID == "":
|
case r.Method == http.MethodPut && subPath != "":
|
||||||
|
api.Store(w, r, subPath)
|
||||||
|
case r.Method == http.MethodPost && subPath == "":
|
||||||
|
api.Store(w, r, "")
|
||||||
|
case r.Method == http.MethodDelete && subPath != "":
|
||||||
|
api.Delete(w, r, subPath)
|
||||||
|
case r.Method == http.MethodGet && subPath == "":
|
||||||
api.List(w, r)
|
api.List(w, r)
|
||||||
case r.Method == http.MethodPost:
|
|
||||||
api.Create(w, r)
|
|
||||||
default:
|
default:
|
||||||
Error(w, http.StatusNotFound, "unregistered path", fmt.Errorf("method %q with subpath %q was not registered in /movie", r.Method, movieID), logger)
|
Error(w, http.StatusNotFound, "unregistered path", fmt.Errorf("method %q with subpath %q was not registered in /movie", r.Method, subPath), logger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +68,7 @@ func (api *MovieAPI) Read(w http.ResponseWriter, r *http.Request, movieID string
|
||||||
fmt.Fprint(w, string(resJson))
|
fmt.Fprint(w, string(resJson))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *MovieAPI) Create(w http.ResponseWriter, r *http.Request) {
|
func (api *MovieAPI) Store(w http.ResponseWriter, r *http.Request, urlID string) {
|
||||||
logger := api.logger.With("method", "create")
|
logger := api.logger.With("method", "create")
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
|
@ -83,14 +78,23 @@ func (api *MovieAPI) Create(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
|
|
||||||
var movie *Movie
|
var movie *movie.Movie
|
||||||
if err := json.Unmarshal(body, &movie); err != nil {
|
if err := json.Unmarshal(body, &movie); err != nil {
|
||||||
Error(w, http.StatusBadRequest, "could not unmarshal request body", err, logger)
|
Error(w, http.StatusBadRequest, "could not unmarshal request body", err, logger)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
movie.ID = uuid.New().String()
|
|
||||||
|
|
||||||
if err := api.repo.StoreMovie(movie); err != nil {
|
switch {
|
||||||
|
case urlID == "" && movie.ID == "":
|
||||||
|
movie.ID = uuid.New().String()
|
||||||
|
case urlID != "" && movie.ID == "":
|
||||||
|
movie.ID = urlID
|
||||||
|
case urlID != "" && movie.ID != "" && urlID != movie.ID:
|
||||||
|
Error(w, http.StatusBadRequest, "id in path does not match id in body", err, logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := api.repo.Store(movie); err != nil {
|
||||||
Error(w, http.StatusInternalServerError, "could not store movie", err, logger)
|
Error(w, http.StatusInternalServerError, "could not store movie", err, logger)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -104,6 +108,17 @@ func (api *MovieAPI) Create(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprint(w, string(resBody))
|
fmt.Fprint(w, string(resBody))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *MovieAPI) Delete(w http.ResponseWriter, r *http.Request, urlID string) {
|
||||||
|
logger := api.logger.With("method", "delete")
|
||||||
|
|
||||||
|
if err := api.repo.Delete(urlID); err != nil {
|
||||||
|
Error(w, http.StatusInternalServerError, "could not delete movie", err, logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
func (api *MovieAPI) List(w http.ResponseWriter, r *http.Request) {
|
func (api *MovieAPI) List(w http.ResponseWriter, r *http.Request) {
|
||||||
logger := api.logger.With("method", "list")
|
logger := api.logger.With("method", "list")
|
||||||
|
|
||||||
|
@ -120,5 +135,4 @@ func (api *MovieAPI) List(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprint(w, string(resBody))
|
fmt.Fprint(w, string(resBody))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,27 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"ewintr.nl/emdb/movie"
|
||||||
|
"github.com/google/uuid"
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
type sqliteMigration string
|
type sqliteMigration string
|
||||||
|
|
||||||
var sqliteMigrations = []sqliteMigration{
|
var sqliteMigrations = []sqliteMigration{
|
||||||
`CREATE TABLE movie ("id" TEXT UNIQUE, "title" TEXT, "year" INTEGER, "imdb_id" TEXT, "watched_on" TEXT, "rating" INTEGER, "comment" TEXT)`,
|
`CREATE TABLE movie (
|
||||||
|
"id" TEXT UNIQUE NOT NULL,
|
||||||
|
"imdb_id" TEXT NOT NULL DEFAULT "",
|
||||||
|
"title" TEXT NOT NULL DEFAULT "",
|
||||||
|
"english_title" TEXT NOT NULL DEFAULT "",
|
||||||
|
"year" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"directors" TEXT NOT NULL DEFAULT "",
|
||||||
|
"watched_on" TEXT NOT NULL DEFAULT "",
|
||||||
|
"rating" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"comment" TEXT NOT NULL DEFAULT ""
|
||||||
|
)`,
|
||||||
`CREATE TABLE system ("latest_sync" INTEGER)`,
|
`CREATE TABLE system ("latest_sync" INTEGER)`,
|
||||||
`INSERT INTO system (latest_sync) VALUES (0)`,
|
`INSERT INTO system (latest_sync) VALUES (0)`,
|
||||||
}
|
}
|
||||||
|
@ -44,16 +57,21 @@ func NewSQLite(dbPath string) (*SQLite, error) {
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SQLite) StoreMovie(movie *Movie) error {
|
func (s *SQLite) Store(m *movie.Movie) error {
|
||||||
|
if m.ID == "" {
|
||||||
|
m.ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
tx, err := s.db.Begin()
|
tx, err := s.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
if _, err := s.db.Exec(`REPLACE INTO movie (id, title, year, imdb_id, watched_on, rating, comment)
|
directors := strings.Join(m.Directors, ",")
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
if _, err := s.db.Exec(`REPLACE INTO movie (id, imdb_id, title, english_title, year, directors, watched_on, rating, comment)
|
||||||
movie.ID, movie.Title, movie.Year, movie.IMDBID, movie.WatchedOn, movie.Rating, movie.Comment); err != nil {
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
m.ID, m.IMDBID, m.Title, m.EnglishTitle, m.Year, directors, m.WatchedOn, m.Rating, m.Comment); err != nil {
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,50 +82,53 @@ func (s *SQLite) StoreMovie(movie *Movie) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SQLite) FindOne(id string) (*Movie, error) {
|
func (s *SQLite) Delete(id string) error {
|
||||||
|
if _, err := s.db.Exec(`DELETE FROM movie WHERE id=?`, id); err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SQLite) FindOne(id string) (*movie.Movie, error) {
|
||||||
row := s.db.QueryRow(`
|
row := s.db.QueryRow(`
|
||||||
SELECT title, year, imdb_id, watched_on, rating, comment
|
SELECT imdb_id, title, english_title, year, directors, watched_on, rating, comment
|
||||||
FROM movie
|
FROM movie
|
||||||
WHERE id=?`, id)
|
WHERE id=?`, id)
|
||||||
if row.Err() != nil {
|
if row.Err() != nil {
|
||||||
return nil, row.Err()
|
return nil, row.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
movie := &Movie{
|
m := &movie.Movie{
|
||||||
ID: id,
|
ID: id,
|
||||||
}
|
}
|
||||||
if err := row.Scan(&movie.Title, &movie.Year, &movie.IMDBID, &movie.WatchedOn, &movie.Rating, &movie.Comment); err != nil {
|
var directors string
|
||||||
return nil, err
|
if err := row.Scan(&m.IMDBID, &m.Title, &m.EnglishTitle, &m.Year, &directors, &m.WatchedOn, &m.Rating, &m.Comment); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
}
|
||||||
|
m.Directors = strings.Split(directors, ",")
|
||||||
|
|
||||||
return movie, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SQLite) FindAll() ([]*Movie, error) {
|
func (s *SQLite) FindAll() ([]*movie.Movie, error) {
|
||||||
rows, err := s.db.Query(`
|
rows, err := s.db.Query(`
|
||||||
SELECT id, title, year, imdb_id, watched_on, rating, comment
|
SELECT imdb_id, title, english_title, year, directors, watched_on, rating, comment
|
||||||
FROM movie`)
|
FROM movie`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
movies := make([]*Movie, 0)
|
movies := make([]*movie.Movie, 0)
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var id, title, imdbID, watchedOn, comment string
|
m := &movie.Movie{}
|
||||||
var year, rating int
|
var directors string
|
||||||
if err := rows.Scan(&id, &title, &year, &imdbID, &watchedOn, &rating, &comment); err != nil {
|
if err := rows.Scan(&m.IMDBID, &m.Title, &m.EnglishTitle, &m.Year, &directors, &m.WatchedOn, &m.Rating, &m.Comment); err != nil {
|
||||||
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
}
|
||||||
movies = append(movies, &Movie{
|
m.Directors = strings.Split(directors, ",")
|
||||||
ID: id,
|
movies = append(movies, m)
|
||||||
Title: title,
|
|
||||||
Year: year,
|
|
||||||
IMDBID: imdbID,
|
|
||||||
WatchedOn: watchedOn,
|
|
||||||
Rating: rating,
|
|
||||||
Comment: comment,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return movies, nil
|
return movies, nil
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package movie
|
||||||
|
|
||||||
|
type Movie struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
IMDBID string `json:"imdbID"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
EnglishTitle string `json:"englishTitle"`
|
||||||
|
Year int `json:"year"`
|
||||||
|
Directors []string `json:"directors"`
|
||||||
|
WatchedOn string `json:"watchedOn"`
|
||||||
|
Rating int `json:"rating"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MovieRepository interface {
|
||||||
|
Store(movie *Movie) error
|
||||||
|
FindOne(id string) (*Movie, error)
|
||||||
|
FindAll() ([]*Movie, error)
|
||||||
|
Delete(id string) error
|
||||||
|
}
|
Loading…
Reference in New Issue