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