Move everything to backend

This commit is contained in:
2024-10-27 23:00:51 +01:00
parent 4d21c77f99
commit a74f722c58
11 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
package main
import (
"fmt"
"log"
)
type AssociationService struct {
db *DB
}
type AssociationServiceQuery struct {
LHSID *int64 `db:"lhs"`
RHSID *int64 `db:"rhs"`
NoteID *int64 `db:"note"`
ID *int64 `db:"id"`
}
func (as *AssociationService) Query(query AssociationServiceQuery) ([]Association, error) {
res := []Association{}
sqlQuery := "SELECT id, lhs, rhs, note FROM association"
whereQuery := BuildWhereQuery(query)
if whereQuery != "" {
sqlQuery += " " + whereQuery
}
rows, err := as.db.readConn.Query(sqlQuery)
if err != nil {
return res, fmt.Errorf("failed getting associations for query %q: %v", sqlQuery, err)
}
defer rows.Close()
for rows.Next() {
association := Association{}
err := rows.Scan(&association.ID, &association.LHS.ID, &association.RHS.ID, &association.Note)
if err != nil {
return res, fmt.Errorf("failed scanning association: %v", err)
}
res = append(res, association)
}
return res, nil
}
func (as *AssociationService) Create(lhs Player, rhs Player, note string) (Association, error) {
log.Printf("Creating association between %d and %d with note %s", lhs.ID, rhs.ID, note)
association := Association{}
res, err := as.db.writeConn.Exec("insert into association (lhs, rhs, note) values (?, ?, ?)", lhs.ID, rhs.ID, note)
if err != nil {
return association, fmt.Errorf("failed to insert association: %v", err)
}
id, err := res.LastInsertId()
if err != nil {
return association, fmt.Errorf("failed to get last insert id: %v", err)
}
log.Printf("Created association between %d and %d with note %s with id %d", lhs.ID, rhs.ID, note, id)
qres, err := as.Query(AssociationServiceQuery{ID: &id})
if err != nil {
return association, fmt.Errorf("failed getting association for id %d: %v", id, err)
}
if len(qres) > 1 {
return association, fmt.Errorf("more than one association found for id %d", id)
}
return qres[0], nil
}

84
backend/db.go Normal file
View File

@@ -0,0 +1,84 @@
package main
import (
"database/sql"
"fmt"
"log"
"os"
"time"
_ "github.com/mattn/go-sqlite3"
)
type DB struct {
Ready bool
path string
readConn *sql.DB
writeConn *sql.DB
}
func (db *DB) Open() error {
if db.path == "" {
return fmt.Errorf("database path not set")
}
file, err := os.Open(db.path)
if err != nil {
if os.IsNotExist(err) {
log.Printf("Database file does not exist at %s, creating", db.path)
file, err := os.Create(db.path)
if err != nil {
return fmt.Errorf("failed to create database file: %v", err)
}
log.Printf("Database created at %s", db.path)
file.Close()
} else {
return fmt.Errorf("failed to open database file: %v", err)
}
}
file.Close()
writeConn, err := sql.Open("sqlite3", db.path+"?_journal=WAL&_synchronous=NORMAL")
if err != nil {
Error.Printf("%++v", err)
return err
}
writeConn.SetMaxOpenConns(1)
writeConn.SetConnMaxIdleTime(30 * time.Second)
writeConn.SetConnMaxLifetime(30 * time.Second)
db.writeConn = writeConn
readConn, err := sql.Open("sqlite3", db.path+"?mode=ro&_journal=WAL&_synchronous=NORMAL&_mode=ro")
if err != nil {
Error.Printf("%++v", err)
return err
}
readConn.SetMaxOpenConns(4)
readConn.SetConnMaxIdleTime(30 * time.Second)
readConn.SetConnMaxLifetime(30 * time.Second)
db.readConn = readConn
db.Ready = true
return nil
}
func (db *DB) Init(ddl string) error {
if !db.Ready {
return fmt.Errorf("database not ready")
}
return nil
}
func (db *DB) Close() error {
err := db.writeConn.Close()
if err != nil {
return err
}
err = db.readConn.Close()
if err != nil {
return err
}
return nil
}

27
backend/ddl.sql Normal file
View File

@@ -0,0 +1,27 @@
create table guild (
id integer primary key,
name text
);
create unique index idx_guild_name on guild(name);
create table player (
id integer primary key,
name text,
guild integer references guild(id)
);
create unique index idx_player_name on player(name);
create table association (
id integer primary key,
lhs integer references player(id),
rhs integer references player(id),
note text
);
create table note (
id integer primary key,
content text,
timestamp string,
player integer references player(id)
);
create unique index idx_note_content on note(timestamp, player);

5
backend/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module stinkinator
go 1.23.2
require github.com/mattn/go-sqlite3 v1.14.24

2
backend/go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=

83
backend/guildService.go Normal file
View File

@@ -0,0 +1,83 @@
package main
import (
"fmt"
"log"
)
type GuildService struct {
db *DB
}
type GuildServiceQuery struct {
Name *string `db:"name"`
ID *int64 `db:"id"`
}
func (gs *GuildService) Query(query GuildServiceQuery) ([]Guild, error) {
res := []Guild{}
sqlQuery := "SELECT id, name FROM guild"
whereQuery := BuildWhereQuery(query)
if whereQuery != "" {
sqlQuery += " " + whereQuery
}
rows, err := gs.db.readConn.Query(sqlQuery)
if err != nil {
return res, fmt.Errorf("failed getting guilds for query %q: %v", sqlQuery, err)
}
defer rows.Close()
for rows.Next() {
guild := Guild{}
err := rows.Scan(&guild.ID, &guild.Name)
if err != nil {
return res, fmt.Errorf("failed scanning guild: %v", err)
}
res = append(res, guild)
}
return res, nil
}
func (gs *GuildService) Create(name string) (Guild, error) {
log.Printf("Creating guild %s", name)
guild := Guild{}
res, err := gs.db.writeConn.Exec("insert into guild (name) values (?)", name)
if err != nil {
return guild, fmt.Errorf("failed to insert guild: %v", err)
}
id, err := res.LastInsertId()
if err != nil {
return guild, fmt.Errorf("failed to get last insert id: %v", err)
}
log.Printf("Created guild %s with id %d", name, id)
qres, err := gs.Query(GuildServiceQuery{ID: &id})
if err != nil {
return guild, fmt.Errorf("failed getting guild for id %d: %v", id, err)
}
if len(qres) > 1 {
return guild, fmt.Errorf("more than one guild found for id %d", id)
}
return qres[0], nil
}
func (gs *GuildService) GetOrCreate(name string) (Guild, error) {
guild := Guild{}
guilds, err := gs.Query(GuildServiceQuery{Name: &name})
if len(guilds) > 1 {
return guild, fmt.Errorf("more than one guild found for name %s", name)
}
if err != nil || len(guilds) == 0 {
guild, err = gs.Create(name)
if err != nil {
return guild, fmt.Errorf("failed creating guild: %v", err)
}
}
if len(guilds) == 1 {
guild = guilds[0]
}
return guild, nil
}

78
backend/main.go Normal file
View File

@@ -0,0 +1,78 @@
package main
import (
"flag"
"fmt"
"io"
"log"
"os"
)
var Error *log.Logger
var Warning *log.Logger
func init() {
log.SetFlags(log.Lmicroseconds | log.Lshortfile)
logFile, err := os.Create("main.log")
if err != nil {
log.Printf("Error creating log file: %v", err)
os.Exit(1)
}
logger := io.MultiWriter(os.Stdout, logFile)
log.SetOutput(logger)
Error = log.New(io.MultiWriter(logFile, os.Stderr, os.Stdout),
fmt.Sprintf("%sERROR:%s ", "\033[0;101m", "\033[0m"),
log.Lmicroseconds|log.Lshortfile)
Warning = log.New(io.MultiWriter(logFile, os.Stdout),
fmt.Sprintf("%sWarning:%s ", "\033[0;93m", "\033[0m"),
log.Lmicroseconds|log.Lshortfile)
}
var db DB
var gs GuildService
var ps PlayerService
var ns NoteService
var as AssociationService
func main() {
inputFile := flag.String("if", "input", "Input file")
flag.Parse()
log.Printf("Input file: %s", *inputFile)
db = DB{
path: "data/db.db",
}
err := db.Open()
if err != nil {
Error.Printf("Failed opening database: %v", err)
return
}
defer db.Close()
gs = GuildService{db: &db}
ps = PlayerService{db: &db}
ns = NoteService{db: &db}
as = AssociationService{db: &db}
inputData, err := os.ReadFile(*inputFile)
if err != nil {
Error.Printf("Failed reading input file: %v", err)
return
}
log.Printf("%#v", string(inputData))
note, err := ParseNote(string(inputData))
if err != nil {
Error.Printf("Failed parsing note: %v", err)
return
}
dbnote, ass, err := PersistNoteData(note)
if err != nil {
Error.Printf("Failed persisting note: %v", err)
return
}
log.Printf("%#v", dbnote)
log.Printf("%#v", ass)
}

72
backend/noteService.go Normal file
View File

@@ -0,0 +1,72 @@
package main
import (
"fmt"
"log"
"time"
)
type NoteService struct {
db *DB
}
type NoteServiceQuery struct {
Timestamp *string `db:"timestamp"`
PlayerID *int64 `db:"player"`
ID *int64 `db:"id"`
}
func (ns *NoteService) Query(query NoteServiceQuery) ([]Note, error) {
res := []Note{}
sqlQuery := "SELECT id, content, timestamp, player FROM note"
whereQuery := BuildWhereQuery(query)
if whereQuery != "" {
sqlQuery += " " + whereQuery
}
rows, err := ns.db.readConn.Query(sqlQuery)
if err != nil {
return res, fmt.Errorf("failed getting notes for query %q: %v", sqlQuery, err)
}
defer rows.Close()
for rows.Next() {
note := Note{}
var ts string
err := rows.Scan(&note.ID, &note.Content, &ts, &note.Player.ID)
if err != nil {
return res, fmt.Errorf("failed scanning note: %v", err)
}
note.Timestamp, err = time.Parse(time.RFC3339, ts)
if err != nil {
return res, fmt.Errorf("failed parsing timestamp: %v", err)
}
res = append(res, note)
}
return res, nil
}
func (ns *NoteService) Create(content string, timestamp time.Time, player Player) (Note, error) {
log.Printf("Creating note %q with timestamp %s and player %d", content, timestamp, player.ID)
note := Note{}
res, err := ns.db.writeConn.Exec("insert into note (content, timestamp, player) values (?, ?, ?)", content, timestamp.Format(time.RFC3339), player.ID)
if err != nil {
return note, fmt.Errorf("failed to insert note: %v", err)
}
id, err := res.LastInsertId()
if err != nil {
return note, fmt.Errorf("failed to get last insert id: %v", err)
}
log.Printf("Created note %q with timestamp %s and player %d with id %d", content, timestamp, player.ID, id)
qres, err := ns.Query(NoteServiceQuery{ID: &id})
if err != nil {
return note, fmt.Errorf("failed getting note for id %d: %v", id, err)
}
if len(qres) > 1 {
return note, fmt.Errorf("more than one note found for id %d", id)
}
return qres[0], nil
}

83
backend/playerService.go Normal file
View File

@@ -0,0 +1,83 @@
package main
import (
"fmt"
"log"
)
type PlayerService struct {
db *DB
}
type PlayerServiceQuery struct {
Name *string `db:"name"`
ID *int64 `db:"id"`
}
func (ps *PlayerService) Query(query PlayerServiceQuery) ([]Player, error) {
res := []Player{}
sqlQuery := "SELECT id, name, guild FROM player"
whereQuery := BuildWhereQuery(query)
if whereQuery != "" {
sqlQuery += " " + whereQuery
}
rows, err := ps.db.readConn.Query(sqlQuery)
if err != nil {
return res, fmt.Errorf("failed getting players for query %q: %v", sqlQuery, err)
}
defer rows.Close()
for rows.Next() {
player := Player{}
err := rows.Scan(&player.ID, &player.Name, &player.Guild.ID)
if err != nil {
return res, fmt.Errorf("failed scanning player: %v", err)
}
res = append(res, player)
}
return res, nil
}
func (ps *PlayerService) Create(name string, guild Guild) (Player, error) {
log.Printf("Creating player %s in guild %s", name, guild.Name)
player := Player{}
res, err := ps.db.writeConn.Exec("insert into player (name, guild) values (?, ?)", name, guild.ID)
if err != nil {
return player, fmt.Errorf("failed to insert player: %v", err)
}
id, err := res.LastInsertId()
if err != nil {
return player, fmt.Errorf("failed to get last insert id: %v", err)
}
log.Printf("Created player %s in guild %s with id %d", name, guild.Name, id)
qres, err := ps.Query(PlayerServiceQuery{ID: &id})
if err != nil {
return player, fmt.Errorf("failed getting player for id %d: %v", id, err)
}
if len(qres) > 1 {
return player, fmt.Errorf("more than one player found for id %d", id)
}
return qres[0], nil
}
func (ps *PlayerService) GetOrCreate(name string, guild Guild) (Player, error) {
player := Player{}
players, err := ps.Query(PlayerServiceQuery{Name: &name})
if len(players) > 1 {
return player, fmt.Errorf("more than one player found for name %s", name)
}
if err != nil || len(players) == 0 {
player, err = ps.Create(name, guild)
if err != nil {
return player, fmt.Errorf("failed creating player: %v", err)
}
}
if len(players) == 1 {
player = players[0]
}
return player, nil
}

39
backend/types.go Normal file
View File

@@ -0,0 +1,39 @@
package main
import "time"
type (
Guild struct {
ID int64
Name string `json:"name"`
}
Player struct {
ID int64
Name string `json:"name"`
Guild Guild `json:"guild"`
}
Association struct {
ID int64
LHS Player
RHS Player
Note string
}
Note struct {
ID int64
Content string
Timestamp time.Time
Player Player
}
)
type NoteData struct {
Player string
Date time.Time
Guild string
Note string
Associations []NoteAssociationData
}
type NoteAssociationData struct {
Player string
Note string
}

116
backend/utils.go Normal file
View File

@@ -0,0 +1,116 @@
package main
import (
"fmt"
"reflect"
"regexp"
"strings"
"time"
)
func BuildWhereQuery(params interface{}) string {
conditions := []string{}
v := reflect.ValueOf(params)
t := v.Type()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if field.Kind() == reflect.Ptr && !field.IsNil() {
dbTag := t.Field(i).Tag.Get("db")
if dbTag != "" {
switch field.Elem().Kind() {
case reflect.String:
conditions = append(conditions, fmt.Sprintf("%s = '%s'", dbTag, field.Elem().String()))
case reflect.Bool:
conditions = append(conditions, fmt.Sprintf("%s = %t", dbTag, field.Elem().Bool()))
case reflect.Int:
conditions = append(conditions, fmt.Sprintf("%s = %d", dbTag, field.Elem().Int()))
case reflect.Int64:
conditions = append(conditions, fmt.Sprintf("%s = %d", dbTag, field.Elem().Int()))
}
}
}
}
if len(conditions) > 0 {
return "WHERE " + strings.Join(conditions, " AND ")
}
return ""
}
var associationRe = regexp.MustCompile(`r:(\w+)(?:\(([^)]+)\))?`)
func ParseNote(data string) (NoteData, error) {
res := NoteData{}
lines := strings.Split(data, "\n")
note := []string{}
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
parts := strings.Split(line, ":")
switch parts[0] {
case "p":
res.Player = parts[1]
case "d":
var err error
res.Date, err = time.Parse(time.DateOnly, parts[1])
if err != nil {
return res, fmt.Errorf("failed to parse date: %v", err)
}
case "g":
res.Guild = parts[1]
default:
note = append(note, line)
}
}
res.Note = strings.Join(note, "\n")
matches := associationRe.FindAllStringSubmatch(res.Note, -1)
for _, match := range matches {
res.Associations = append(res.Associations, NoteAssociationData{
Player: match[1],
Note: match[2],
})
}
return res, nil
}
func PersistNoteData(note NoteData) (Note, []Association, error) {
res := Note{}
ass := []Association{}
res.Content = note.Note
res.Timestamp = note.Date
guild, err := gs.GetOrCreate(note.Guild)
if err != nil {
return res, ass, fmt.Errorf("failed getting guild for %s: %v", note.Guild, err)
}
player, err := ps.GetOrCreate(note.Player, guild)
if err != nil {
return res, ass, fmt.Errorf("failed getting player for %s: %v", note.Player, err)
}
res.Player = player
player.Guild = guild
for _, assoc := range note.Associations {
assocPlayer, err := ps.GetOrCreate(assoc.Player, guild)
if err != nil {
return res, ass, fmt.Errorf("failed getting player for %s: %v", assoc.Player, err)
}
association, err := as.Create(player, assocPlayer, assoc.Note)
if err != nil {
return res, ass, fmt.Errorf("failed creating association: %v", err)
}
ass = append(ass, association)
}
res, err = ns.Create(res.Content, res.Timestamp, res.Player)
if err != nil {
return res, ass, fmt.Errorf("failed creating note: %v", err)
}
return res, ass, nil
}