remove api service
This commit is contained in:
parent
987a83e760
commit
b13037b882
|
@ -1,37 +0,0 @@
|
||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ContextKey string
|
|
||||||
|
|
||||||
const (
|
|
||||||
MovieKey = ContextKey("movie")
|
|
||||||
)
|
|
||||||
|
|
||||||
func Index(w http.ResponseWriter) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
fmt.Fprint(w, `{"message":"emdb index"}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Error(w http.ResponseWriter, status int, message string, err error, logger *slog.Logger) {
|
|
||||||
logger.Error(message, "error", err)
|
|
||||||
|
|
||||||
w.WriteHeader(status)
|
|
||||||
|
|
||||||
var resBody []byte
|
|
||||||
res := struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
}{
|
|
||||||
Message: message,
|
|
||||||
Error: err.Error(),
|
|
||||||
}
|
|
||||||
resBody, _ = json.Marshal(res)
|
|
||||||
|
|
||||||
fmt.Fprint(w, string(resBody))
|
|
||||||
}
|
|
|
@ -1,120 +0,0 @@
|
||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"code.ewintr.nl/emdb/job"
|
|
||||||
)
|
|
||||||
|
|
||||||
type JobAPI struct {
|
|
||||||
jq *job.JobQueue
|
|
||||||
logger *slog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewJobAPI(jq *job.JobQueue, logger *slog.Logger) *JobAPI {
|
|
||||||
return &JobAPI{
|
|
||||||
jq: jq,
|
|
||||||
logger: logger.With("api", "admin"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (jobAPI *JobAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := jobAPI.logger.With("method", "serveHTTP")
|
|
||||||
|
|
||||||
subPath, _ := ShiftPath(r.URL.Path)
|
|
||||||
switch {
|
|
||||||
case r.Method == http.MethodPost && subPath == "":
|
|
||||||
jobAPI.Add(w, r)
|
|
||||||
case r.Method == http.MethodGet && subPath == "":
|
|
||||||
jobAPI.List(w, r)
|
|
||||||
case r.Method == http.MethodGet && subPath == "next-ai":
|
|
||||||
jobAPI.NextAI(w, r)
|
|
||||||
case r.Method == http.MethodDelete && subPath != "":
|
|
||||||
jobAPI.Delete(w, r, subPath)
|
|
||||||
case r.Method == http.MethodDelete && subPath == "":
|
|
||||||
jobAPI.DeleteAll(w, r)
|
|
||||||
default:
|
|
||||||
Error(w, http.StatusNotFound, "unregistered path", fmt.Errorf("method %q with subpath %q was not registered in /movie", r.Method, subPath), logger)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (jobAPI *JobAPI) Add(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := jobAPI.logger.With("method", "add")
|
|
||||||
|
|
||||||
var j job.Job
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&j); err != nil {
|
|
||||||
Error(w, http.StatusBadRequest, "could not decode job", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := jobAPI.jq.Add(j.ActionID, j.Action); err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not add job", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(j); err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not encode job", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (jobAPI *JobAPI) List(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := jobAPI.logger.With("method", "list")
|
|
||||||
|
|
||||||
jobs, err := jobAPI.jq.List()
|
|
||||||
if err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not list jobs", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(jobs); err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not encode jobs", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (jobAPI *JobAPI) NextAI(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := jobAPI.logger.With("method", "nextai")
|
|
||||||
|
|
||||||
j, err := jobAPI.jq.Next(job.TypeAI)
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, sql.ErrNoRows):
|
|
||||||
logger.Info("no ai jobs found")
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
case err != nil:
|
|
||||||
Error(w, http.StatusInternalServerError, "could not get next ai job", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(j); err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not encode job", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (jobAPI *JobAPI) Delete(w http.ResponseWriter, r *http.Request, id string) {
|
|
||||||
logger := jobAPI.logger.With("method", "delete")
|
|
||||||
|
|
||||||
if err := jobAPI.jq.Delete(id); err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not delete job", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (jobAPI *JobAPI) DeleteAll(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := jobAPI.logger.With("method", "deleteall")
|
|
||||||
|
|
||||||
if err := jobAPI.jq.DeleteAll(); err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not delete all jobs", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
|
@ -1,159 +0,0 @@
|
||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"code.ewintr.nl/emdb/job"
|
|
||||||
"code.ewintr.nl/emdb/storage"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MovieAPI struct {
|
|
||||||
apis APIIndex
|
|
||||||
repo *storage.MovieRepository
|
|
||||||
jq *job.JobQueue
|
|
||||||
logger *slog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMovieAPI(apis APIIndex, repo *storage.MovieRepository, jq *job.JobQueue, logger *slog.Logger) *MovieAPI {
|
|
||||||
return &MovieAPI{
|
|
||||||
apis: apis,
|
|
||||||
repo: repo,
|
|
||||||
logger: logger.With("api", "movie"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (movieAPI *MovieAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := movieAPI.logger.With("method", "serveHTTP")
|
|
||||||
|
|
||||||
head, tail := ShiftPath(r.URL.Path)
|
|
||||||
subHead, subTail := ShiftPath(tail)
|
|
||||||
for aPath, api := range movieAPI.apis {
|
|
||||||
if head != "" && subHead == fmt.Sprintf("%s", aPath) {
|
|
||||||
r.URL.Path = subTail
|
|
||||||
r = r.Clone(context.WithValue(r.Context(), MovieKey, head))
|
|
||||||
api.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case r.Method == http.MethodGet && head != "":
|
|
||||||
movieAPI.Read(w, r, head)
|
|
||||||
case r.Method == http.MethodPut && head != "":
|
|
||||||
movieAPI.Store(w, r, head)
|
|
||||||
case r.Method == http.MethodPost && head == "":
|
|
||||||
movieAPI.Store(w, r, "")
|
|
||||||
case r.Method == http.MethodDelete && head != "":
|
|
||||||
movieAPI.Delete(w, r, head)
|
|
||||||
case r.Method == http.MethodGet && head == "":
|
|
||||||
movieAPI.List(w, r)
|
|
||||||
default:
|
|
||||||
Error(w, http.StatusNotFound, "unregistered path", fmt.Errorf("method %q with subpath %q was not registered in /movie", r.Method, head), logger)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (movieAPI *MovieAPI) Read(w http.ResponseWriter, r *http.Request, movieID string) {
|
|
||||||
logger := movieAPI.logger.With("method", "read")
|
|
||||||
|
|
||||||
m, err := movieAPI.repo.FindOne(movieID)
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, sql.ErrNoRows):
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
fmt.Fprint(w, `{"message":"not found"}`)
|
|
||||||
return
|
|
||||||
case err != nil:
|
|
||||||
Error(w, http.StatusInternalServerError, "could not get movie", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resJson, err := json.Marshal(m)
|
|
||||||
if err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not marshal response", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprint(w, string(resJson))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (movieAPI *MovieAPI) Store(w http.ResponseWriter, r *http.Request, urlID string) {
|
|
||||||
logger := movieAPI.logger.With("method", "create")
|
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
Error(w, http.StatusBadRequest, "could not read body", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
var m storage.Movie
|
|
||||||
if err := json.Unmarshal(body, &m); err != nil {
|
|
||||||
Error(w, http.StatusBadRequest, "could not unmarshal request body", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case urlID == "" && m.ID == "":
|
|
||||||
m.ID = uuid.New().String()
|
|
||||||
case urlID != "" && m.ID == "":
|
|
||||||
m.ID = urlID
|
|
||||||
case urlID != "" && m.ID != "" && urlID != m.ID:
|
|
||||||
Error(w, http.StatusBadRequest, "id in path does not match id in body", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := movieAPI.repo.Store(m); err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not store movie", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resBody, err := json.Marshal(m)
|
|
||||||
if err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not marshal movie", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprint(w, string(resBody))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (movieAPI *MovieAPI) Delete(w http.ResponseWriter, r *http.Request, urlID string) {
|
|
||||||
logger := movieAPI.logger.With("method", "delete")
|
|
||||||
|
|
||||||
err := movieAPI.repo.Delete(urlID)
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, sql.ErrNoRows):
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
fmt.Fprint(w, `{"message":"not found"}`)
|
|
||||||
return
|
|
||||||
case err != nil:
|
|
||||||
Error(w, http.StatusInternalServerError, "could not delete movie", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (movieAPI *MovieAPI) List(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := movieAPI.logger.With("method", "list")
|
|
||||||
|
|
||||||
movies, err := movieAPI.repo.FindAll()
|
|
||||||
if err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not get movies", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resBody, err := json.Marshal(movies)
|
|
||||||
if err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not marshal movies", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprint(w, string(resBody))
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"code.ewintr.nl/emdb/storage"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MovieReviewAPI struct {
|
|
||||||
repo *storage.ReviewRepository
|
|
||||||
logger *slog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMovieReviewAPI(repo *storage.ReviewRepository, logger *slog.Logger) *MovieReviewAPI {
|
|
||||||
return &MovieReviewAPI{
|
|
||||||
repo: repo,
|
|
||||||
logger: logger.With("api", "moviereview"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (reviewAPI *MovieReviewAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := reviewAPI.logger.With("method", "serveHTTP")
|
|
||||||
|
|
||||||
subPath, _ := ShiftPath(r.URL.Path)
|
|
||||||
switch {
|
|
||||||
case r.Method == http.MethodGet && subPath == "":
|
|
||||||
reviewAPI.List(w, r)
|
|
||||||
default:
|
|
||||||
Error(w, http.StatusNotFound, "unregistered path", fmt.Errorf("method %q with subpath %q was not registered in /review", r.Method, subPath), logger)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (reviewAPI *MovieReviewAPI) List(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := reviewAPI.logger.With("method", "list")
|
|
||||||
|
|
||||||
movieID := r.Context().Value(MovieKey).(string)
|
|
||||||
reviews, err := reviewAPI.repo.FindByMovieID(movieID)
|
|
||||||
if err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not get reviews", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(reviews); err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not encode reviews", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,159 +0,0 @@
|
||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"code.ewintr.nl/emdb/storage"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ReviewAPI struct {
|
|
||||||
repo *storage.ReviewRepository
|
|
||||||
logger *slog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewReviewAPI(repo *storage.ReviewRepository, logger *slog.Logger) *ReviewAPI {
|
|
||||||
return &ReviewAPI{
|
|
||||||
repo: repo,
|
|
||||||
logger: logger.With("api", "review"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (reviewAPI *ReviewAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := reviewAPI.logger.With("method", "serveHTTP")
|
|
||||||
|
|
||||||
subPath, subTrail := ShiftPath(r.URL.Path)
|
|
||||||
subSubPath, _ := ShiftPath(subTrail)
|
|
||||||
switch {
|
|
||||||
case r.Method == http.MethodGet && subPath == "":
|
|
||||||
reviewAPI.List(w, r)
|
|
||||||
case r.Method == http.MethodGet && subPath == "unrated" && subSubPath == "":
|
|
||||||
reviewAPI.ListUnrated(w, r)
|
|
||||||
case r.Method == http.MethodGet && subPath == "unrated" && subSubPath == "next":
|
|
||||||
reviewAPI.NextUnrated(w, r)
|
|
||||||
case r.Method == http.MethodGet && subPath == "no-titles" && subSubPath == "":
|
|
||||||
reviewAPI.ListNoTitles(w, r)
|
|
||||||
case r.Method == http.MethodGet && subPath == "no-titles" && subSubPath == "next":
|
|
||||||
reviewAPI.NextNoTitles(w, r)
|
|
||||||
case r.Method == http.MethodGet && subPath != "":
|
|
||||||
reviewAPI.Get(w, r, subPath)
|
|
||||||
case r.Method == http.MethodPut && subPath != "":
|
|
||||||
reviewAPI.Store(w, r, subPath)
|
|
||||||
default:
|
|
||||||
Error(w, http.StatusNotFound, "unregistered path", fmt.Errorf("method %q with subpath %q was not registered in /review", r.Method, subPath), logger)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (reviewAPI *ReviewAPI) Get(w http.ResponseWriter, r *http.Request, id string) {
|
|
||||||
logger := reviewAPI.logger.With("method", "get")
|
|
||||||
|
|
||||||
review, err := reviewAPI.repo.FindOne(id)
|
|
||||||
if err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not get review", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(review); err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not encode review", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (reviewAPI *ReviewAPI) List(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := reviewAPI.logger.With("method", "list")
|
|
||||||
|
|
||||||
reviews, err := reviewAPI.repo.FindAll()
|
|
||||||
if err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not get reviews", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(reviews); err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not encode reviews", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (reviewAPI *ReviewAPI) ListUnrated(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := reviewAPI.logger.With("method", "listUnrated")
|
|
||||||
|
|
||||||
reviews, err := reviewAPI.repo.FindUnrated()
|
|
||||||
if err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not get reviews", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(reviews); err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not encode reviews", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (reviewAPI *ReviewAPI) NextUnrated(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := reviewAPI.logger.With("method", "nextUnrated")
|
|
||||||
|
|
||||||
review, err := reviewAPI.repo.FindNextUnrated()
|
|
||||||
if err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not get review", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(review); err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not encode review", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (reviewAPI *ReviewAPI) ListNoTitles(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := reviewAPI.logger.With("method", "listNoTitles")
|
|
||||||
|
|
||||||
reviews, err := reviewAPI.repo.FindNoTitles()
|
|
||||||
if err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not get reviews", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(reviews); err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not encode reviews", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (reviewAPI *ReviewAPI) NextNoTitles(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger := reviewAPI.logger.With("method", "nextNoTitles")
|
|
||||||
|
|
||||||
review, err := reviewAPI.repo.FindNextNoTitles()
|
|
||||||
if err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not get review", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(review); err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not encode review", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (reviewAPI *ReviewAPI) Store(w http.ResponseWriter, r *http.Request, id string) {
|
|
||||||
logger := reviewAPI.logger.With("method", "store")
|
|
||||||
|
|
||||||
var review storage.Review
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&review); err != nil {
|
|
||||||
Error(w, http.StatusBadRequest, "could not decode review", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if id != review.ID {
|
|
||||||
Error(w, http.StatusBadRequest, "id in path does not match id in body", fmt.Errorf("id in path %q does not match id in body %q", id, review.ID), logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := reviewAPI.repo.Store(review); err != nil {
|
|
||||||
Error(w, http.StatusInternalServerError, "could not store review", err, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type APIIndex map[string]http.Handler
|
|
||||||
|
|
||||||
type Server struct {
|
|
||||||
apiKey string
|
|
||||||
apis map[string]http.Handler
|
|
||||||
logger *slog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewServer(apiKey string, apis map[string]http.Handler, logger *slog.Logger) *Server {
|
|
||||||
return &Server{
|
|
||||||
apiKey: apiKey,
|
|
||||||
apis: apis,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
rec := httptest.NewRecorder() // records the response to be able to mix writing headers and content
|
|
||||||
|
|
||||||
// cors
|
|
||||||
rec.Header().Add("Access-Control-Allow-Origin", "*")
|
|
||||||
rec.Header().Add("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
|
||||||
rec.Header().Add("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
||||||
if r.Method == http.MethodOptions {
|
|
||||||
rec.WriteHeader(http.StatusOK)
|
|
||||||
returnResponse(w, rec, r, s.logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger := s.logger.With("path", r.URL.Path)
|
|
||||||
rec.Header().Add("Content-Type", "application/json")
|
|
||||||
|
|
||||||
// authenticate
|
|
||||||
if key := r.Header.Get("Authorization"); s.apiKey != "localOnly" && key != s.apiKey {
|
|
||||||
Error(rec, http.StatusUnauthorized, "unauthorized", fmt.Errorf("invalid api key"), logger)
|
|
||||||
logger.Info("unauthorized", "key", key)
|
|
||||||
returnResponse(w, rec, r, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// route to internal
|
|
||||||
head, tail := ShiftPath(r.URL.Path)
|
|
||||||
if len(head) == 0 {
|
|
||||||
Index(rec)
|
|
||||||
returnResponse(w, rec, r, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
api, ok := s.apis[head]
|
|
||||||
if !ok {
|
|
||||||
Error(rec, http.StatusNotFound, "Not found", fmt.Errorf("%s is not a valid path", r.URL.Path), logger)
|
|
||||||
returnResponse(w, rec, r, logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r.URL.Path = tail
|
|
||||||
api.ServeHTTP(rec, r)
|
|
||||||
returnResponse(w, rec, r, logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
func returnResponse(w http.ResponseWriter, rec *httptest.ResponseRecorder, r *http.Request, logger *slog.Logger) {
|
|
||||||
for k, v := range rec.Header() {
|
|
||||||
w.Header()[k] = v
|
|
||||||
}
|
|
||||||
w.WriteHeader(rec.Code)
|
|
||||||
w.Write(rec.Body.Bytes())
|
|
||||||
logger.Info("request served", "method", r.Method, "status", rec.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShiftPath splits off the first component of p, which will be cleaned of
|
|
||||||
// relative components before processing. head will never contain a slash and
|
|
||||||
// tail will always be a rooted path without trailing slash.
|
|
||||||
// See https://blog.merovius.de/posts/2017-06-18-how-not-to-use-an-http-router/
|
|
||||||
func ShiftPath(p string) (string, string) {
|
|
||||||
p = path.Clean("/" + p)
|
|
||||||
i := strings.Index(p[1:], "/") + 1
|
|
||||||
if i <= 0 {
|
|
||||||
return p[1:], "/"
|
|
||||||
}
|
|
||||||
return p[1:i], p[i:]
|
|
||||||
}
|
|
|
@ -1,224 +0,0 @@
|
||||||
package moviestore
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
_ "modernc.org/sqlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
type sqliteMigration string
|
|
||||||
|
|
||||||
var sqliteMigrations = []sqliteMigration{
|
|
||||||
`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)`,
|
|
||||||
`INSERT INTO system (latest_sync) VALUES (0)`,
|
|
||||||
`ALTER TABLE movie ADD COLUMN tmdb_id INTEGER NOT NULL DEFAULT 0`,
|
|
||||||
`ALTER TABLE movie ADD COLUMN summary TEXT NOT NULL DEFAULT ""`,
|
|
||||||
`BEGIN TRANSACTION;
|
|
||||||
CREATE TABLE movie_new (
|
|
||||||
"id" TEXT UNIQUE NOT NULL,
|
|
||||||
"imdb_id" TEXT UNIQUE NOT NULL DEFAULT "",
|
|
||||||
"tmdb_id" INTEGER UNIQUE NOT NULL DEFAULT 0,
|
|
||||||
"title" TEXT NOT NULL DEFAULT "",
|
|
||||||
"english_title" TEXT NOT NULL DEFAULT "",
|
|
||||||
"year" INTEGER NOT NULL DEFAULT 0,
|
|
||||||
"directors" TEXT NOT NULL DEFAULT "",
|
|
||||||
"summary" TEXT NOT NULL DEFAULT "",
|
|
||||||
"watched_on" TEXT NOT NULL DEFAULT "",
|
|
||||||
"rating" INTEGER NOT NULL DEFAULT 0,
|
|
||||||
"comment" TEXT NOT NULL DEFAULT ""
|
|
||||||
);
|
|
||||||
INSERT INTO movie_new (id, imdb_id, tmdb_id, title, english_title, year, directors, summary, watched_on, rating, comment)
|
|
||||||
SELECT id, imdb_id, tmdb_id, title, english_title, year, directors, summary, watched_on, rating, comment FROM movie;
|
|
||||||
DROP TABLE movie;
|
|
||||||
ALTER TABLE movie_new RENAME TO movie;
|
|
||||||
COMMIT`,
|
|
||||||
`CREATE TABLE review (
|
|
||||||
"id" TEXT UNIQUE NOT NULL,
|
|
||||||
"movie_id" TEXT NOT NULL,
|
|
||||||
"source" TEXT NOT NULL DEFAULT "",
|
|
||||||
"url" TEXT NOT NULL DEFAULT "",
|
|
||||||
"review" TEXT NOT NULL DEFAULT ""
|
|
||||||
)`,
|
|
||||||
`CREATE TABLE job_queue (
|
|
||||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
"movie_id" TEXT NOT NULL,
|
|
||||||
"action" TEXT NOT NULL DEFAULT "",
|
|
||||||
"status" TEXT NOT NULL DEFAULT ""
|
|
||||||
)`,
|
|
||||||
`PRAGMA journal_mode=WAL`,
|
|
||||||
`INSERT INTO job_queue (movie_id, action, status)
|
|
||||||
SELECT id, 'fetch-imdb-reviews', 'todo'
|
|
||||||
FROM movie`,
|
|
||||||
`AlTER TABLE review ADD COLUMN "references" TEXT NOT NULL DEFAULT ""`,
|
|
||||||
`ALTER TABLE review ADD COLUMN "quality" INTEGER NOT NULL DEFAULT 0`,
|
|
||||||
`ALTER TABLE review DROP COLUMN "references"`,
|
|
||||||
`ALTER TABLE review ADD COLUMN "mentions" TEXT NOT NULL DEFAULT ""`,
|
|
||||||
`ALTER TABLE review ADD COLUMN "movie_rating" INTEGER NOT NULL DEFAULT 0`,
|
|
||||||
`BEGIN TRANSACTION;
|
|
||||||
CREATE TABLE job_queue_new (
|
|
||||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
"movie_id" TEXT NOT NULL,
|
|
||||||
"action" TEXT NOT NULL DEFAULT "",
|
|
||||||
"status" TEXT NOT NULL DEFAULT "",
|
|
||||||
"created_at" DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
INSERT INTO job_queue_new (id, movie_id, action, status)
|
|
||||||
SELECT id, movie_id, action, status FROM job_queue;
|
|
||||||
DROP TABLE job_queue;
|
|
||||||
ALTER TABLE job_queue_new RENAME TO job_queue;
|
|
||||||
COMMIT`,
|
|
||||||
`CREATE TRIGGER set_timestamp_after_insert
|
|
||||||
AFTER INSERT ON job_queue
|
|
||||||
BEGIN
|
|
||||||
UPDATE job_queue SET created_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE rowid = new.rowid;
|
|
||||||
END;
|
|
||||||
CREATE TRIGGER set_timestamp_after_update
|
|
||||||
AFTER UPDATE ON job_queue
|
|
||||||
BEGIN
|
|
||||||
UPDATE job_queue SET updated_at = CURRENT_TIMESTAMP WHERE rowid = old.rowid;
|
|
||||||
END;`,
|
|
||||||
`ALTER TABLE review ADD COLUMN "mentioned_titles" JSON`,
|
|
||||||
`ALTER TABLE review DROP COLUMN "mentions"`,
|
|
||||||
`ALTER TABLE review DROP COLUMN "mentioned_titles"`,
|
|
||||||
`ALTER TABLE review ADD COLUMN "mentioned_titles" JSON NOT NULL Default '{}'`,
|
|
||||||
`BEGIN TRANSACTION;
|
|
||||||
CREATE TABLE job_queue_new (
|
|
||||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
"action_id" TEXT NOT NULL,
|
|
||||||
"action" TEXT NOT NULL DEFAULT "",
|
|
||||||
"status" TEXT NOT NULL DEFAULT "",
|
|
||||||
"created_at" DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
INSERT INTO job_queue_new (id, action_id, action, status)
|
|
||||||
SELECT id, movie_id, action, status FROM job_queue;
|
|
||||||
DROP TABLE job_queue;
|
|
||||||
ALTER TABLE job_queue_new RENAME TO job_queue;
|
|
||||||
COMMIT`,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInvalidConfiguration = errors.New("invalid configuration")
|
|
||||||
ErrIncompatibleSQLMigration = errors.New("incompatible migration")
|
|
||||||
ErrNotEnoughSQLMigrations = errors.New("already more migrations than wanted")
|
|
||||||
ErrSqliteFailure = errors.New("sqlite returned an error")
|
|
||||||
)
|
|
||||||
|
|
||||||
type SQLite struct {
|
|
||||||
db *sql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSQLite(dbPath string) (*SQLite, error) {
|
|
||||||
db, err := sql.Open("sqlite", dbPath)
|
|
||||||
if err != nil {
|
|
||||||
return &SQLite{}, fmt.Errorf("%w: %v", ErrInvalidConfiguration, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = db.Exec(fmt.Sprintf("PRAGMA busy_timeout=%d;", 5*time.Second))
|
|
||||||
|
|
||||||
s := &SQLite{
|
|
||||||
db: db,
|
|
||||||
}
|
|
||||||
|
|
||||||
//if err := s.migrate(sqliteMigrations); err != nil {
|
|
||||||
// return &SQLite{}, err
|
|
||||||
//}
|
|
||||||
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLite) Exec(query string, args ...any) (sql.Result, error) {
|
|
||||||
return s.db.Exec(query, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLite) QueryRow(query string, args ...any) *sql.Row {
|
|
||||||
return s.db.QueryRow(query, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SQLite) Query(query string, args ...any) (*sql.Rows, error) {
|
|
||||||
return s.db.Query(query, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
//func (s *SQLite) migrate(wanted []sqliteMigration) error {
|
|
||||||
// // admin table
|
|
||||||
// if _, err := s.db.Exec(`
|
|
||||||
//CREATE TABLE IF NOT EXISTS migration
|
|
||||||
//("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "query" TEXT)
|
|
||||||
//`); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // find existing
|
|
||||||
// rows, err := s.db.Query(`SELECT query FROM migration ORDER BY id`)
|
|
||||||
// if err != nil {
|
|
||||||
// return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// existing := []sqliteMigration{}
|
|
||||||
// for rows.Next() {
|
|
||||||
// var query string
|
|
||||||
// if err := rows.Scan(&query); err != nil {
|
|
||||||
// return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
// }
|
|
||||||
// existing = append(existing, sqliteMigration(query))
|
|
||||||
// }
|
|
||||||
// rows.Close()
|
|
||||||
//
|
|
||||||
// // compare
|
|
||||||
// missing, err := compareMigrations(wanted, existing)
|
|
||||||
// if err != nil {
|
|
||||||
// return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // execute missing
|
|
||||||
// for _, query := range missing {
|
|
||||||
// if _, err := s.db.Exec(string(query)); err != nil {
|
|
||||||
// return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // register
|
|
||||||
// if _, err := s.db.Exec(`
|
|
||||||
//INSERT INTO migration
|
|
||||||
//(query) VALUES (?)
|
|
||||||
//`, query); err != nil {
|
|
||||||
// return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return nil
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//func compareMigrations(wanted, existing []sqliteMigration) ([]sqliteMigration, error) {
|
|
||||||
// needed := []sqliteMigration{}
|
|
||||||
// if len(wanted) < len(existing) {
|
|
||||||
// return []sqliteMigration{}, ErrNotEnoughSQLMigrations
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// for i, want := range wanted {
|
|
||||||
// switch {
|
|
||||||
// case i >= len(existing):
|
|
||||||
// needed = append(needed, want)
|
|
||||||
// case want == existing[i]:
|
|
||||||
// // do nothing
|
|
||||||
// case want != existing[i]:
|
|
||||||
// return []sqliteMigration{}, fmt.Errorf("%w: %v", ErrIncompatibleSQLMigration, want)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return needed, nil
|
|
||||||
//}
|
|
|
@ -1,56 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"code.ewintr.nl/emdb/client"
|
|
||||||
"code.ewintr.nl/emdb/cmd/api-service/handler"
|
|
||||||
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
|
|
||||||
job2 "code.ewintr.nl/emdb/job"
|
|
||||||
"code.ewintr.nl/emdb/storage"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
port = flag.Int("port", 8085, "port to listen on")
|
|
||||||
dbPath = flag.String("dbpath", "test.db", "path to sqlite db")
|
|
||||||
apiKey = flag.String("apikey", "hoi", "api key to use")
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
|
||||||
logger.Info("starting server", "port", *port, "dbPath", *dbPath)
|
|
||||||
|
|
||||||
db, err := moviestore.NewSQLite(*dbPath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("could not create new sqlite repo: %s", err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
jobQueue := job2.NewJobQueue(db, logger)
|
|
||||||
worker := job2.NewWorker(jobQueue, storage.NewMovieRepository(db), storage.NewReviewRepository(db), client.NewIMDB(), logger)
|
|
||||||
go worker.Run()
|
|
||||||
|
|
||||||
apis := handler.APIIndex{
|
|
||||||
"job": handler.NewJobAPI(jobQueue, logger),
|
|
||||||
"movie": handler.NewMovieAPI(handler.APIIndex{
|
|
||||||
"review": handler.NewMovieReviewAPI(storage.NewReviewRepository(db), logger),
|
|
||||||
}, storage.NewMovieRepository(db), jobQueue, logger),
|
|
||||||
"review": handler.NewReviewAPI(storage.NewReviewRepository(db), logger),
|
|
||||||
}
|
|
||||||
|
|
||||||
go http.ListenAndServe(fmt.Sprintf(":%d", *port), handler.NewServer(*apiKey, apis, logger))
|
|
||||||
logger.Info("server started")
|
|
||||||
|
|
||||||
c := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
<-c
|
|
||||||
|
|
||||||
logger.Info("server stopped")
|
|
||||||
}
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -53,7 +52,7 @@ SET
|
||||||
rating = EXCLUDED.rating,
|
rating = EXCLUDED.rating,
|
||||||
comment = EXCLUDED.comment;`,
|
comment = EXCLUDED.comment;`,
|
||||||
m.ID, m.TMDBID, m.IMDBID, m.Title, m.EnglishTitle, m.Year, directors, m.Summary, m.WatchedOn, m.Rating, m.Comment); err != nil {
|
m.ID, m.TMDBID, m.IMDBID, m.Title, m.EnglishTitle, m.Year, directors, m.Summary, m.WatchedOn, m.Rating, m.Comment); err != nil {
|
||||||
return fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err)
|
return fmt.Errorf("%w: %v", ErrPostgresqlFailure, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -61,7 +60,7 @@ SET
|
||||||
|
|
||||||
func (mr *MovieRepository) Delete(id string) error {
|
func (mr *MovieRepository) Delete(id string) error {
|
||||||
if _, err := mr.db.Exec(`DELETE FROM movie WHERE id=$1`, id); err != nil {
|
if _, err := mr.db.Exec(`DELETE FROM movie WHERE id=$1`, id); err != nil {
|
||||||
return fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err)
|
return fmt.Errorf("%w: %v", ErrPostgresqlFailure, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -81,7 +80,7 @@ WHERE id=$1`, id)
|
||||||
}
|
}
|
||||||
var directors string
|
var directors string
|
||||||
if err := row.Scan(&m.ID, &m.TMDBID, &m.IMDBID, &m.Title, &m.EnglishTitle, &m.Year, &directors, &m.Summary, &m.WatchedOn, &m.Rating, &m.Comment); err != nil {
|
if err := row.Scan(&m.ID, &m.TMDBID, &m.IMDBID, &m.Title, &m.EnglishTitle, &m.Year, &directors, &m.Summary, &m.WatchedOn, &m.Rating, &m.Comment); err != nil {
|
||||||
return Movie{}, fmt.Errorf("%w: %w", moviestore.ErrSqliteFailure, err)
|
return Movie{}, fmt.Errorf("%w: %w", ErrPostgresqlFailure, err)
|
||||||
}
|
}
|
||||||
m.Directors = strings.Split(directors, ",")
|
m.Directors = strings.Split(directors, ",")
|
||||||
|
|
||||||
|
@ -93,7 +92,7 @@ func (mr *MovieRepository) FindAll() ([]Movie, error) {
|
||||||
SELECT id, tmdb_id, imdb_id, title, english_title, year, directors, summary, watched_on, rating, comment
|
SELECT id, tmdb_id, imdb_id, title, english_title, year, directors, summary, watched_on, rating, comment
|
||||||
FROM movie`)
|
FROM movie`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err)
|
return nil, fmt.Errorf("%w: %v", ErrPostgresqlFailure, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
movies := make([]Movie, 0)
|
movies := make([]Movie, 0)
|
||||||
|
@ -102,7 +101,7 @@ FROM movie`)
|
||||||
m := Movie{}
|
m := Movie{}
|
||||||
var directors string
|
var directors string
|
||||||
if err := rows.Scan(&m.ID, &m.TMDBID, &m.IMDBID, &m.Title, &m.EnglishTitle, &m.Year, &directors, &m.Summary, &m.WatchedOn, &m.Rating, &m.Comment); err != nil {
|
if err := rows.Scan(&m.ID, &m.TMDBID, &m.IMDBID, &m.Title, &m.EnglishTitle, &m.Year, &directors, &m.Summary, &m.WatchedOn, &m.Rating, &m.Comment); err != nil {
|
||||||
return nil, fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err)
|
return nil, fmt.Errorf("%w: %v", ErrPostgresqlFailure, err)
|
||||||
}
|
}
|
||||||
m.Directors = strings.Split(directors, ",")
|
m.Directors = strings.Split(directors, ",")
|
||||||
movies = append(movies, m)
|
movies = append(movies, m)
|
||||||
|
|
|
@ -2,12 +2,18 @@ package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
|
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrPostgresqlFailure = errors.New("postgresql failure")
|
||||||
|
ErrNotEnoughSQLMigrations = errors.New("not enough sql migrations")
|
||||||
|
ErrIncompatibleSQLMigration = errors.New("incompatible sql migration")
|
||||||
|
)
|
||||||
|
|
||||||
type migration string
|
type migration string
|
||||||
|
|
||||||
var migrations = []migration{
|
var migrations = []migration{
|
||||||
|
@ -95,14 +101,14 @@ CREATE TABLE IF NOT EXISTS migration
|
||||||
// find existing
|
// find existing
|
||||||
rows, err := pg.db.Query(`SELECT query FROM migration ORDER BY id`)
|
rows, err := pg.db.Query(`SELECT query FROM migration ORDER BY id`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err)
|
return fmt.Errorf("%w: %v", ErrPostgresqlFailure, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
existing := []migration{}
|
existing := []migration{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var query string
|
var query string
|
||||||
if err := rows.Scan(&query); err != nil {
|
if err := rows.Scan(&query); err != nil {
|
||||||
return fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err)
|
return fmt.Errorf("%w: %v", ErrPostgresqlFailure, err)
|
||||||
}
|
}
|
||||||
existing = append(existing, migration(query))
|
existing = append(existing, migration(query))
|
||||||
}
|
}
|
||||||
|
@ -111,13 +117,13 @@ CREATE TABLE IF NOT EXISTS migration
|
||||||
// compare
|
// compare
|
||||||
missing, err := compareMigrations(wanted, existing)
|
missing, err := compareMigrations(wanted, existing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err)
|
return fmt.Errorf("%w: %v", ErrPostgresqlFailure, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// execute missing
|
// execute missing
|
||||||
for _, query := range missing {
|
for _, query := range missing {
|
||||||
if _, err := pg.db.Exec(string(query)); err != nil {
|
if _, err := pg.db.Exec(string(query)); err != nil {
|
||||||
return fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err)
|
return fmt.Errorf("%w: %v", ErrPostgresqlFailure, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// register
|
// register
|
||||||
|
@ -125,7 +131,7 @@ CREATE TABLE IF NOT EXISTS migration
|
||||||
INSERT INTO migration
|
INSERT INTO migration
|
||||||
(query) VALUES ($1)
|
(query) VALUES ($1)
|
||||||
`, query); err != nil {
|
`, query); err != nil {
|
||||||
return fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err)
|
return fmt.Errorf("%w: %v", ErrPostgresqlFailure, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,7 +153,7 @@ func (pg *Postgres) Query(query string, args ...any) (*sql.Rows, error) {
|
||||||
func compareMigrations(wanted, existing []migration) ([]migration, error) {
|
func compareMigrations(wanted, existing []migration) ([]migration, error) {
|
||||||
needed := []migration{}
|
needed := []migration{}
|
||||||
if len(wanted) < len(existing) {
|
if len(wanted) < len(existing) {
|
||||||
return []migration{}, moviestore.ErrNotEnoughSQLMigrations
|
return []migration{}, ErrNotEnoughSQLMigrations
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, want := range wanted {
|
for i, want := range wanted {
|
||||||
|
@ -157,7 +163,7 @@ func compareMigrations(wanted, existing []migration) ([]migration, error) {
|
||||||
case want == existing[i]:
|
case want == existing[i]:
|
||||||
// do nothing
|
// do nothing
|
||||||
case want != existing[i]:
|
case want != existing[i]:
|
||||||
return []migration{}, fmt.Errorf("%w: %v", moviestore.ErrIncompatibleSQLMigration, want)
|
return []migration{}, fmt.Errorf("%w: %v", ErrIncompatibleSQLMigration, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue