156 lines
4 KiB
Go
156 lines
4 KiB
Go
package timeline
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func (tl *Timeline) GetProperties(ctx context.Context) (map[string]any, error) {
|
|
properties := make(map[string]any)
|
|
|
|
rows, err := tl.db.ReadPool.QueryContext(ctx, "SELECT key, value, type FROM repo")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("querying properties: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var key string
|
|
var valStr, valType *string
|
|
err := rows.Scan(&key, &valStr, &valType)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("scanning row: %w", err)
|
|
}
|
|
value, err := tl.readProperty(valStr, valType)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("property %s: %w", key, err)
|
|
}
|
|
properties[key] = value
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("scanning rows: %w", err)
|
|
}
|
|
|
|
return properties, nil
|
|
}
|
|
|
|
func (*Timeline) readProperty(valStr, valType *string) (any, error) {
|
|
if valType == nil {
|
|
return nil, errors.New("data type missing")
|
|
}
|
|
var value any
|
|
var err error
|
|
if valStr != nil && valType != nil {
|
|
switch *valType {
|
|
case "bool":
|
|
switch strings.ToLower(*valStr) {
|
|
case "0", "false", "", "no", "off", "disabled", "f":
|
|
value = false
|
|
case "1", "true", "yes", "on", "enabled", "t":
|
|
value = true
|
|
}
|
|
case "int":
|
|
value, err = strconv.ParseInt(*valStr, 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing property as %s: %w (value=%s)", *valType, err, *valStr)
|
|
}
|
|
case "float64":
|
|
value, err = strconv.ParseFloat(*valStr, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing property as %s: %w (value=%s)", *valType, err, *valStr)
|
|
}
|
|
case "string":
|
|
value = *valStr
|
|
}
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (tl *Timeline) LoadProperty(ctx context.Context, key string) (any, error) {
|
|
var valStr, valType *string
|
|
err := tl.db.ReadPool.QueryRowContext(ctx, "SELECT value, type FROM repo WHERE key=? LIMIT 1", key).Scan(&valStr, &valType)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("querying for property %s: %w", key, err)
|
|
}
|
|
value, err := tl.readProperty(valStr, valType)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("property %s: %w", key, err)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (tl *Timeline) GetProperty(ctx context.Context, key string) any {
|
|
value, err := tl.LoadProperty(ctx, key)
|
|
if err != nil {
|
|
Log.Named("timeline").Error("getting property",
|
|
zap.String("key", key),
|
|
zap.Error(err))
|
|
return nil
|
|
}
|
|
return value
|
|
}
|
|
|
|
func (tl *Timeline) SetProperties(ctx context.Context, properties map[string]any) error {
|
|
tx, err := tl.db.WritePool.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("starting tx: %w", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
for key, value := range properties {
|
|
// can't change repo ID or version
|
|
if key == "id" || key == "version" {
|
|
continue
|
|
}
|
|
|
|
// nil values should just delete the row entirely
|
|
if isNil(value) {
|
|
_, err := tx.ExecContext(ctx, `DELETE FROM repo WHERE key=?`, key)
|
|
if err != nil {
|
|
return fmt.Errorf("deleting property %s: %w", key, err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// integers decoded from JSON will be float64 because ... JSON... so
|
|
// try to convert them back to ints if possible
|
|
if fl, ok := value.(float64); ok && float64(int(fl)) == fl {
|
|
value = int(fl)
|
|
}
|
|
|
|
valType := fmt.Sprintf("%T", value) // commonly: int, bool, string, float64
|
|
|
|
// to avoid ambiguity in the DB, store bools as true/false strings,
|
|
// otherwise they just go in as 1/0 strings instead -- yes, we store
|
|
// the type, but I think true/false is more intuitive
|
|
if b, ok := value.(bool); ok {
|
|
if b {
|
|
value = "true"
|
|
} else {
|
|
value = "false"
|
|
}
|
|
}
|
|
|
|
_, err := tx.ExecContext(ctx, `INSERT INTO repo (key, value, type) VALUES (?, ?, ?)
|
|
ON CONFLICT DO UPDATE SET key=excluded.key, value=excluded.value, type=excluded.type`,
|
|
key, value, valType)
|
|
if err != nil {
|
|
return fmt.Errorf("inserting/updating property %s=%v (%s): %w", key, value, valType, err)
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return fmt.Errorf("committing tx: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|