remove api service

This commit is contained in:
Erik Winter 2024-03-09 13:23:15 +01:00
parent 987a83e760
commit b13037b882
10 changed files with 19 additions and 910 deletions

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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:]
}

View File

@ -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
//}

View File

@ -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")
}

View File

@ -4,7 +4,6 @@ import (
"fmt"
"strings"
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
"github.com/google/uuid"
)
@ -53,7 +52,7 @@ SET
rating = EXCLUDED.rating,
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 {
return fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err)
return fmt.Errorf("%w: %v", ErrPostgresqlFailure, err)
}
return nil
@ -61,7 +60,7 @@ SET
func (mr *MovieRepository) Delete(id string) error {
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
@ -81,7 +80,7 @@ WHERE id=$1`, id)
}
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 {
return Movie{}, fmt.Errorf("%w: %w", moviestore.ErrSqliteFailure, err)
return Movie{}, fmt.Errorf("%w: %w", ErrPostgresqlFailure, err)
}
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
FROM movie`)
if err != nil {
return nil, fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err)
return nil, fmt.Errorf("%w: %v", ErrPostgresqlFailure, err)
}
movies := make([]Movie, 0)
@ -102,7 +101,7 @@ FROM movie`)
m := Movie{}
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 {
return nil, fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err)
return nil, fmt.Errorf("%w: %v", ErrPostgresqlFailure, err)
}
m.Directors = strings.Split(directors, ",")
movies = append(movies, m)

View File

@ -2,12 +2,18 @@ package storage
import (
"database/sql"
"errors"
"fmt"
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
_ "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
var migrations = []migration{
@ -95,14 +101,14 @@ CREATE TABLE IF NOT EXISTS migration
// find existing
rows, err := pg.db.Query(`SELECT query FROM migration ORDER BY id`)
if err != nil {
return fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err)
return fmt.Errorf("%w: %v", ErrPostgresqlFailure, err)
}
existing := []migration{}
for rows.Next() {
var query string
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))
}
@ -111,13 +117,13 @@ CREATE TABLE IF NOT EXISTS migration
// compare
missing, err := compareMigrations(wanted, existing)
if err != nil {
return fmt.Errorf("%w: %v", moviestore.ErrSqliteFailure, err)
return fmt.Errorf("%w: %v", ErrPostgresqlFailure, err)
}
// execute missing
for _, query := range missing {
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
@ -125,7 +131,7 @@ CREATE TABLE IF NOT EXISTS migration
INSERT INTO migration
(query) VALUES ($1)
`, 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) {
needed := []migration{}
if len(wanted) < len(existing) {
return []migration{}, moviestore.ErrNotEnoughSQLMigrations
return []migration{}, ErrNotEnoughSQLMigrations
}
for i, want := range wanted {
@ -157,7 +163,7 @@ func compareMigrations(wanted, existing []migration) ([]migration, error) {
case want == existing[i]:
// do nothing
case want != existing[i]:
return []migration{}, fmt.Errorf("%w: %v", moviestore.ErrIncompatibleSQLMigration, want)
return []migration{}, fmt.Errorf("%w: %v", ErrIncompatibleSQLMigration, want)
}
}