depot/web/barf/frontend/barfdb/barfdb.go

197 lines
5.4 KiB
Go
Raw Normal View History

package barfdb
import (
"context"
"crypto/rand"
"database/sql"
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"strings"
)
type Dataset struct {
CurrentPhase int `json:"currentPhase"`
CurrentPhaseDialog int `json:"currentPhaseDialog"`
AudioEnabled bool `json:"audioEnabled"`
Name string `json:"name"`
DiscordUsername string `json:"discordUsername"`
ReceiveEmail bool `json:"receiveEmail"`
Email string `json:"email"`
DateSat31August string `json:"dateSat31August"`
DateSat7September string `json:"dateSat7September"`
ActivityEscapeRoom string `json:"activityEscapeRoom"`
ActivityPub string `json:"activityPub"`
ActivityMeanGirlsTheMusical string `json:"activityMeanGirlsTheMusical"`
ActivityKaraoke string `json:"activityKaraoke"`
AccommodationRequired bool `json:"accommodationRequired"`
TravelCosts bool `json:"travelCosts"`
Misc string `json:"misc"`
}
func (ds *Dataset) Scan(value any) error {
return json.Unmarshal([]byte(value.(string)), ds)
}
func (ds *Dataset) Value() (driver.Value, error) {
b, err := json.Marshal(ds)
return string(b), err
}
func (ds *Dataset) HumanString() (string, error) {
bs, err := json.MarshalIndent(ds, "", " ")
if err != nil {
return "", err
}
return string(bs), nil
}
type DB struct {
db *sql.DB
}
func New(sdb *sql.DB) *DB {
return &DB{sdb}
}
func (d *DB) Close() error {
return d.db.Close()
}
var ErrNoSuchKey = errors.New("barfdb: no such key")
const (
keyBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
keyLength = 12
)
func (d *DB) Initialize(ctx context.Context) error {
_, err := d.db.ExecContext(ctx, "CREATE TABLE responses (key STRING PRIMARY KEY NOT NULL, responses JSONB NOT NULL)")
return err
}
type Entry struct {
FullKey string
Dataset Dataset
}
func (e Entry) BaseKey() string {
if strings.Contains(e.FullKey, "/") {
return e.FullKey[:strings.Index(e.FullKey, "/")]
}
return e.FullKey
}
func (e Entry) Completed() bool {
mustHaveValue := []string{
e.Dataset.DiscordUsername,
e.Dataset.Name,
e.Dataset.ActivityEscapeRoom,
e.Dataset.ActivityKaraoke,
e.Dataset.ActivityMeanGirlsTheMusical,
e.Dataset.ActivityPub,
e.Dataset.DateSat31August,
e.Dataset.DateSat7September,
}
for _, v := range mustHaveValue {
if v == "" {
return false
}
}
return true
}
func (d *DB) List(ctx context.Context) ([]Entry, error) {
rows, err := d.db.QueryContext(ctx, "SELECT key, responses FROM responses ORDER BY key")
if err != nil {
return nil, fmt.Errorf("querying database: %w", err)
}
var entries []Entry
for rows.Next() {
var (
fullKey string
dataset Dataset
)
if err := rows.Scan(&fullKey, &dataset); err != nil {
return nil, fmt.Errorf("scanning row from database: %w", err)
}
entry := Entry{
FullKey: fullKey,
Dataset: dataset,
}
entries = append(entries, entry)
}
return entries, nil
}
func (d *DB) GetOrCreateKey(ctx context.Context, baseKey string, initialDataset *Dataset) (string, error) {
var existingKey string
err := d.db.QueryRowContext(ctx, `SELECT key FROM responses WHERE key LIKE ? || '/%'`, baseKey).Scan(&existingKey)
if err == nil {
return existingKey, nil
} else if errors.Is(err, sql.ErrNoRows) {
// This is fine, we'll create it.
} else {
return "", fmt.Errorf("querying database for existing keys for base %q: %w", baseKey, err)
}
randomKeyBytes := make([]byte, keyLength)
if _, err := rand.Read(randomKeyBytes); err != nil {
return "", fmt.Errorf("generating random key: %w", err)
}
randomKeyStringBytes := make([]byte, keyLength)
for n, b := range randomKeyBytes {
randomKeyStringBytes[n] = keyBytes[int(b)%len(keyBytes)]
}
randomKey := string(randomKeyStringBytes)
fullKey := baseKey + "/" + randomKey
if _, err := d.db.ExecContext(ctx, `INSERT INTO responses (key, responses) VALUES (?, ?)`, fullKey, initialDataset); err != nil {
return "", fmt.Errorf("inserting into responses table: %w", err)
}
return fullKey, nil
}
func (d *DB) GetResponseForShortKey(ctx context.Context, shortKey string) (*Dataset, error) {
var response Dataset
err := d.db.QueryRowContext(ctx, `SELECT responses FROM responses WHERE key = ? OR key LIKE ? || '/%'`, shortKey, shortKey).Scan(&response)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoSuchKey
} else if err != nil {
return nil, fmt.Errorf("fetching %v from database: %w", shortKey, err)
}
return &response, nil
}
func (d *DB) GetResponseForKey(ctx context.Context, key string) (*Dataset, error) {
var response Dataset
err := d.db.QueryRowContext(ctx, `SELECT responses FROM responses WHERE key = ?`, key).Scan(&response)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoSuchKey
} else if err != nil {
return nil, fmt.Errorf("fetching %v from database: %w", key, err)
}
return &response, nil
}
func (d *DB) SaveResponseForKey(ctx context.Context, key string, dataset *Dataset) error {
res, err := d.db.ExecContext(ctx, `UPDATE responses SET responses = ? WHERE key = ?`, dataset, key)
if err != nil {
return fmt.Errorf("updating database for %v: %w", key, err)
}
rowCount, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("determining affected row count for %v: %w", key, err)
} else if rowCount == 0 {
return ErrNoSuchKey
}
return nil
}
func NewDB(db *sql.DB) *DB {
return &DB{db: db}
}