get database migrations up and running

This commit is contained in:
David 2021-07-02 18:13:58 -04:00
parent da239cf9ad
commit 04506ed01f
6 changed files with 227 additions and 43 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -2,5 +2,7 @@ version: "3.8"
services:
mysql:
image: mysql:5.7
ports:
- 3306:3306
environment:
- MYSQL_ROOT_PASSWORD=KigYBNCT9IU5XyB3ehzMLFWyI

View File

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