From 04506ed01f17d5b0a843d12fa5b032a89f3d7131 Mon Sep 17 00:00:00 2001 From: David Ashby Date: Fri, 2 Jul 2021 18:13:58 -0400 Subject: [PATCH] get database migrations up and running --- book/book.go | 34 +++---- cmd/serve/main.go | 75 ++++++++++++++- database/migrations/mysql/01-init.sql | 20 ++++ database/mysql.go | 128 ++++++++++++++++++++++---- docker-compose.yml | 2 + frontend/frontend.go | 11 ++- 6 files changed, 227 insertions(+), 43 deletions(-) diff --git a/book/book.go b/book/book.go index 13fe763..4552e7a 100644 --- a/book/book.go +++ b/book/book.go @@ -1,21 +1,21 @@ package book type Book struct { - ID int - Title string - Authors []string - SortAuthor string - ISBN10 string - ISBN13 string - Format string - Genre string - Publisher string - Series string - Volume string - Year string - Signed bool - Description string - Notes string - OnLoan string - CoverURL string + ID int `json:"-"` + Title string `json:"title"` + Authors []string `json:"authors"` + SortAuthor string `json:"sortAuthor"` + ISBN10 string `json:"isbn-10"` + ISBN13 string `json:"isbn-13"` + Format string `json:"format"` + Genre string `json:"genre"` + Publisher string `json:"publisher"` + Series string `json:"series"` + Volume string `json:"volume"` + Year string `json:"year"` + Signed bool `json:"signed"` + Description string `json:"description"` + Notes string `json:"notes"` + OnLoan string `json:"onLoan"` + CoverURL string `json:"coverURL"` } diff --git a/cmd/serve/main.go b/cmd/serve/main.go index e776d2a..4130001 100644 --- a/cmd/serve/main.go +++ b/cmd/serve/main.go @@ -1,15 +1,80 @@ package main import ( - "fmt" + "context" + "encoding/json" "io/fs" + "log" "net/http" + "git.yetaga.in/alazyreader/library/book" + "git.yetaga.in/alazyreader/library/database" "git.yetaga.in/alazyreader/library/frontend" ) -// test 3 -func main() { - subfs, _ := fs.Sub(frontend.Static, "files") - fmt.Println(http.ListenAndServe(":8080", http.FileServer(http.FS(subfs)))) +type Library interface { + GetAllBooks(context.Context) ([]book.Book, error) +} + +type Router struct { + static fs.FS + lib Library +} + +func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if req.URL.Path == "/api" { + APIHandler(r.lib).ServeHTTP(w, req) + return + } + StaticHandler(r.static).ServeHTTP(w, req) + return +} + +func APIHandler(l Library) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + books, err := l.GetAllBooks(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + b, err := json.Marshal(books) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(b) + w.Write([]byte("\n")) + }) +} + +func StaticHandler(f fs.FS) http.Handler { + return http.FileServer(http.FS(f)) +} + +func main() { + f, err := frontend.Root() + if err != nil { + log.Fatalln(err) + } + lib, err := database.NewMySQLConnection("root", "KigYBNCT9IU5XyB3ehzMLFWyI", "127.0.0.1", "3306", "library") + if err != nil { + log.Fatalln(err) + } + err = lib.PrepareDatabase(context.Background()) + if err != nil { + log.Fatalln(err) + } + latest, run, err := lib.RunMigrations(context.Background()) + if err != nil { + log.Fatalln(err) + } + log.Printf("latest migration: %d; migrations run: %d", latest, run) + r := &Router{ + static: f, + lib: lib, + } + log.Println("Listening on http://localhost:8080/") + log.Fatalln(http.ListenAndServe(":8080", r)) } diff --git a/database/migrations/mysql/01-init.sql b/database/migrations/mysql/01-init.sql index e69de29..a456a86 100644 --- a/database/migrations/mysql/01-init.sql +++ b/database/migrations/mysql/01-init.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS books( + id INT NOT NULL AUTO_INCREMENT, + title VARCHAR(1024), + authors VARCHAR(1024), + sortauthor VARCHAR(1024), + isbn10 VARCHAR(10), + isbn13 VARCHAR(13), + format VARCHAR(255), + genre VARCHAR(255), + publisher VARCHAR(255), + series VARCHAR(255), + volume VARCHAR(255), + year VARCHAR(10), + signed BOOLEAN, + description TEXT, + notes TEXT, + onloan VARCHAR(255), + coverurl VARCHAR(1024), + PRIMARY KEY (id) +) \ No newline at end of file diff --git a/database/mysql.go b/database/mysql.go index b351268..e222dc0 100644 --- a/database/mysql.go +++ b/database/mysql.go @@ -25,6 +25,7 @@ type migration struct { type MySQL struct { connection *sql.DB + tableName string versionTable string migrationsDirectory string } @@ -37,8 +38,9 @@ func NewMySQLConnection(user, pass, host, port, db string) (*MySQL, error) { } return &MySQL{ connection: connection, + tableName: "books", versionTable: "migrations", - migrationsDirectory: "/migrations/mysql", + migrationsDirectory: "migrations/mysql", }, nil } @@ -72,26 +74,26 @@ func (m *MySQL) GetLatestMigration(ctx context.Context) (int, error) { } var latestMigration int - err := m.connection.QueryRowContext(ctx, "SELECT MAX(id) FROM "+m.versionTable).Scan(&latestMigration) + err := m.connection.QueryRowContext(ctx, "SELECT COALESCE(MAX(id), 0) FROM "+m.versionTable).Scan(&latestMigration) return latestMigration, err } -func (m *MySQL) RunMigrations(ctx context.Context) (int, error) { +func (m *MySQL) RunMigrations(ctx context.Context) (int, int, error) { if m.connection == nil || m.migrationsDirectory == "" || m.versionTable == "" { - return 0, fmt.Errorf("uninitialized mysql client") + return 0, 0, fmt.Errorf("uninitialized mysql client") } - var migrations map[int]migration + migrations := map[int]migration{} dir, err := migrationsFS.ReadDir(m.migrationsDirectory) if err != nil { - return 0, nil + return 0, 0, err } for f := range dir { if dir[f].Type().IsRegular() { mig := migration{} id, name, err := parseMigrationFileName(dir[f].Name()) if err != nil { - return 0, err + return 0, 0, err } mig.id, mig.name = id, name mig.content, err = fs.ReadFile(migrationsFS, m.migrationsDirectory+"/"+dir[f].Name()) @@ -101,41 +103,43 @@ func (m *MySQL) RunMigrations(ctx context.Context) (int, error) { latestMigrationRan, err := m.GetLatestMigration(ctx) if err != nil { - return 0, err + return 0, 0, err } // exit if nothing to do (that is, there's no greater migration ID) if _, ok := migrations[latestMigrationRan+1]; !ok { - return latestMigrationRan, nil + return latestMigrationRan, 0, nil } // loop over and apply migrations if required tx, err := m.connection.BeginTx(ctx, nil) if err != nil { - return latestMigrationRan, err + return latestMigrationRan, 0, err } + migrationsRun := 0 for migrationsToRun := true; migrationsToRun; _, migrationsToRun = migrations[latestMigrationRan+1] { mig := migrations[latestMigrationRan+1] _, err := tx.ExecContext(ctx, string(mig.content)) if err != nil { nestederr := tx.Rollback() if nestederr != nil { - return latestMigrationRan, nestederr + return latestMigrationRan, migrationsRun, nestederr } - return latestMigrationRan, err + return latestMigrationRan, migrationsRun, err } _, err = tx.ExecContext(ctx, "INSERT INTO "+m.versionTable+" (id, name, datetime) VALUES (?, ?, ?)", mig.id, mig.name, time.Now()) if err != nil { nestederr := tx.Rollback() if nestederr != nil { - return latestMigrationRan, nestederr + return latestMigrationRan, migrationsRun, nestederr } - return latestMigrationRan, err + return latestMigrationRan, migrationsRun, err } latestMigrationRan = latestMigrationRan + 1 + migrationsRun = migrationsRun + 1 } err = tx.Commit() - return latestMigrationRan, err + return latestMigrationRan, migrationsRun, err } func (m *MySQL) GetAllBooks(ctx context.Context) ([]book.Book, error) { @@ -144,7 +148,25 @@ func (m *MySQL) GetAllBooks(ctx context.Context) ([]book.Book, error) { } books := []book.Book{} - rows, err := m.connection.QueryContext(ctx, "SELECT id, title FROM books") + rows, err := m.connection.QueryContext(ctx, ` + SELECT id, + title, + authors, + sortauthor, + isbn10, + isbn13, + format, + genre, + publisher, + series, + volume, + year, + signed, + description, + notes, + onloan, + coverurl + FROM `+m.tableName) if err != nil { return nil, err } @@ -152,10 +174,18 @@ func (m *MySQL) GetAllBooks(ctx context.Context) ([]book.Book, error) { for rows.Next() { b := book.Book{} - err := rows.Scan(&b.ID, &b.Title) + var authors string + err := rows.Scan( + &b.ID, &b.Title, &authors, + &b.SortAuthor, &b.ISBN10, &b.ISBN13, + &b.Format, &b.Genre, &b.Publisher, + &b.Series, &b.Volume, &b.Year, + &b.Signed, &b.Description, &b.Notes, + &b.OnLoan, &b.CoverURL) if err != nil { return nil, err } + b.Authors = strings.Split(authors, ";") books = append(books, b) } @@ -167,7 +197,28 @@ func (m *MySQL) AddBook(ctx context.Context, b *book.Book) error { return fmt.Errorf("uninitialized mysql client") } - res, err := m.connection.ExecContext(ctx, "INSERT INTO books (title) VALUES (?)", b.Title) + res, err := m.connection.ExecContext(ctx, ` + INSERT INTO `+m.tableName+` + (title, authors, sortauthor, isbn10, isbn13, format, genre, publisher, series, volume, year, signed, description, notes, onloan, coverurl) + VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, )`, + b.Title, + strings.Join(b.Authors, ";"), + b.SortAuthor, + b.ISBN10, + b.ISBN13, + b.Format, + b.Genre, + b.Publisher, + b.Series, + b.Volume, + b.Year, + b.Signed, + b.Description, + b.Notes, + b.OnLoan, + b.CoverURL, + ) if err != nil { return err } @@ -185,8 +236,47 @@ func (m *MySQL) UpdateBook(ctx context.Context, old, new *book.Book) error { if m.connection == nil { return fmt.Errorf("uninitialized mysql client") } + if old.ID != new.ID { + return fmt.Errorf("cannot change book ID") + } - res, err := m.connection.ExecContext(ctx, "UPDATE books SET title=? WHERE id=?", new.Title, old.ID) + res, err := m.connection.ExecContext(ctx, ` + UPDATE `+m.tableName+` + SET id=? + title=? + authors=? + sortauthor=? + isbn10=? + isbn13=? + format=? + genre=? + publisher=? + series=? + volume=? + year=? + signed=? + description=? + notes=? + onloan=? + coverurl=? + WHERE id=?`, + new.Title, + strings.Join(new.Authors, ";"), + new.SortAuthor, + new.ISBN10, + new.ISBN13, + new.Format, + new.Genre, + new.Publisher, + new.Series, + new.Volume, + new.Year, + new.Signed, + new.Description, + new.Notes, + new.OnLoan, + new.CoverURL, + old.ID) if err != nil { return err } diff --git a/docker-compose.yml b/docker-compose.yml index ae1670c..85b835a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,5 +2,7 @@ version: "3.8" services: mysql: image: mysql:5.7 + ports: + - 3306:3306 environment: - MYSQL_ROOT_PASSWORD=KigYBNCT9IU5XyB3ehzMLFWyI diff --git a/frontend/frontend.go b/frontend/frontend.go index 55f5f01..49c6b61 100644 --- a/frontend/frontend.go +++ b/frontend/frontend.go @@ -1,6 +1,13 @@ package frontend -import "embed" +import ( + "embed" + "io/fs" +) //go:embed files -var Static embed.FS +var static embed.FS + +func Root() (fs.FS, error) { + return fs.Sub(static, "files") +}