get database migrations up and running
This commit is contained in:
parent
da239cf9ad
commit
04506ed01f
34
book/book.go
34
book/book.go
@ -1,21 +1,21 @@
|
|||||||
package book
|
package book
|
||||||
|
|
||||||
type Book struct {
|
type Book struct {
|
||||||
ID int
|
ID int `json:"-"`
|
||||||
Title string
|
Title string `json:"title"`
|
||||||
Authors []string
|
Authors []string `json:"authors"`
|
||||||
SortAuthor string
|
SortAuthor string `json:"sortAuthor"`
|
||||||
ISBN10 string
|
ISBN10 string `json:"isbn-10"`
|
||||||
ISBN13 string
|
ISBN13 string `json:"isbn-13"`
|
||||||
Format string
|
Format string `json:"format"`
|
||||||
Genre string
|
Genre string `json:"genre"`
|
||||||
Publisher string
|
Publisher string `json:"publisher"`
|
||||||
Series string
|
Series string `json:"series"`
|
||||||
Volume string
|
Volume string `json:"volume"`
|
||||||
Year string
|
Year string `json:"year"`
|
||||||
Signed bool
|
Signed bool `json:"signed"`
|
||||||
Description string
|
Description string `json:"description"`
|
||||||
Notes string
|
Notes string `json:"notes"`
|
||||||
OnLoan string
|
OnLoan string `json:"onLoan"`
|
||||||
CoverURL string
|
CoverURL string `json:"coverURL"`
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,80 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.yetaga.in/alazyreader/library/book"
|
||||||
|
"git.yetaga.in/alazyreader/library/database"
|
||||||
"git.yetaga.in/alazyreader/library/frontend"
|
"git.yetaga.in/alazyreader/library/frontend"
|
||||||
)
|
)
|
||||||
|
|
||||||
// test 3
|
type Library interface {
|
||||||
func main() {
|
GetAllBooks(context.Context) ([]book.Book, error)
|
||||||
subfs, _ := fs.Sub(frontend.Static, "files")
|
}
|
||||||
fmt.Println(http.ListenAndServe(":8080", http.FileServer(http.FS(subfs))))
|
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
)
|
@ -25,6 +25,7 @@ type migration struct {
|
|||||||
|
|
||||||
type MySQL struct {
|
type MySQL struct {
|
||||||
connection *sql.DB
|
connection *sql.DB
|
||||||
|
tableName string
|
||||||
versionTable string
|
versionTable string
|
||||||
migrationsDirectory string
|
migrationsDirectory string
|
||||||
}
|
}
|
||||||
@ -37,8 +38,9 @@ func NewMySQLConnection(user, pass, host, port, db string) (*MySQL, error) {
|
|||||||
}
|
}
|
||||||
return &MySQL{
|
return &MySQL{
|
||||||
connection: connection,
|
connection: connection,
|
||||||
|
tableName: "books",
|
||||||
versionTable: "migrations",
|
versionTable: "migrations",
|
||||||
migrationsDirectory: "/migrations/mysql",
|
migrationsDirectory: "migrations/mysql",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,26 +74,26 @@ func (m *MySQL) GetLatestMigration(ctx context.Context) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var latestMigration int
|
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
|
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 == "" {
|
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)
|
dir, err := migrationsFS.ReadDir(m.migrationsDirectory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
for f := range dir {
|
for f := range dir {
|
||||||
if dir[f].Type().IsRegular() {
|
if dir[f].Type().IsRegular() {
|
||||||
mig := migration{}
|
mig := migration{}
|
||||||
id, name, err := parseMigrationFileName(dir[f].Name())
|
id, name, err := parseMigrationFileName(dir[f].Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
mig.id, mig.name = id, name
|
mig.id, mig.name = id, name
|
||||||
mig.content, err = fs.ReadFile(migrationsFS, m.migrationsDirectory+"/"+dir[f].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)
|
latestMigrationRan, err := m.GetLatestMigration(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// exit if nothing to do (that is, there's no greater migration ID)
|
// exit if nothing to do (that is, there's no greater migration ID)
|
||||||
if _, ok := migrations[latestMigrationRan+1]; !ok {
|
if _, ok := migrations[latestMigrationRan+1]; !ok {
|
||||||
return latestMigrationRan, nil
|
return latestMigrationRan, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loop over and apply migrations if required
|
// loop over and apply migrations if required
|
||||||
tx, err := m.connection.BeginTx(ctx, nil)
|
tx, err := m.connection.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return latestMigrationRan, err
|
return latestMigrationRan, 0, err
|
||||||
}
|
}
|
||||||
|
migrationsRun := 0
|
||||||
for migrationsToRun := true; migrationsToRun; _, migrationsToRun = migrations[latestMigrationRan+1] {
|
for migrationsToRun := true; migrationsToRun; _, migrationsToRun = migrations[latestMigrationRan+1] {
|
||||||
mig := migrations[latestMigrationRan+1]
|
mig := migrations[latestMigrationRan+1]
|
||||||
_, err := tx.ExecContext(ctx, string(mig.content))
|
_, err := tx.ExecContext(ctx, string(mig.content))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
nestederr := tx.Rollback()
|
nestederr := tx.Rollback()
|
||||||
if nestederr != nil {
|
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())
|
_, err = tx.ExecContext(ctx, "INSERT INTO "+m.versionTable+" (id, name, datetime) VALUES (?, ?, ?)", mig.id, mig.name, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
nestederr := tx.Rollback()
|
nestederr := tx.Rollback()
|
||||||
if nestederr != nil {
|
if nestederr != nil {
|
||||||
return latestMigrationRan, nestederr
|
return latestMigrationRan, migrationsRun, nestederr
|
||||||
}
|
}
|
||||||
return latestMigrationRan, err
|
return latestMigrationRan, migrationsRun, err
|
||||||
}
|
}
|
||||||
latestMigrationRan = latestMigrationRan + 1
|
latestMigrationRan = latestMigrationRan + 1
|
||||||
|
migrationsRun = migrationsRun + 1
|
||||||
}
|
}
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
return latestMigrationRan, err
|
return latestMigrationRan, migrationsRun, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MySQL) GetAllBooks(ctx context.Context) ([]book.Book, error) {
|
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{}
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -152,10 +174,18 @@ func (m *MySQL) GetAllBooks(ctx context.Context) ([]book.Book, error) {
|
|||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
b := book.Book{}
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
b.Authors = strings.Split(authors, ";")
|
||||||
books = append(books, b)
|
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")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -185,8 +236,47 @@ func (m *MySQL) UpdateBook(ctx context.Context, old, new *book.Book) error {
|
|||||||
if m.connection == nil {
|
if m.connection == nil {
|
||||||
return fmt.Errorf("uninitialized mysql client")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -2,5 +2,7 @@ version: "3.8"
|
|||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
image: mysql:5.7
|
image: mysql:5.7
|
||||||
|
ports:
|
||||||
|
- 3306:3306
|
||||||
environment:
|
environment:
|
||||||
- MYSQL_ROOT_PASSWORD=KigYBNCT9IU5XyB3ehzMLFWyI
|
- MYSQL_ROOT_PASSWORD=KigYBNCT9IU5XyB3ehzMLFWyI
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
package frontend
|
package frontend
|
||||||
|
|
||||||
import "embed"
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
)
|
||||||
|
|
||||||
//go:embed files
|
//go:embed files
|
||||||
var Static embed.FS
|
var static embed.FS
|
||||||
|
|
||||||
|
func Root() (fs.FS, error) {
|
||||||
|
return fs.Sub(static, "files")
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user