Files

161 lines
3.2 KiB
Go

package sqlite
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
"inbox/internal/base/idgen"
"inbox/internal/base/timeutil"
)
func (s *Store) now() string {
return timeutil.FormatRFC3339(s.clock.Now())
}
func (s *Store) newID(kind string) (string, error) {
return idgen.NewGenerator(s.clock, nil).New(kind)
}
type versionedConfigCurrent struct {
ID string
Version int
CreatedAt string
}
type versionedConfigMetadata struct {
Action string
ID string
Version int
CreatedAt string
UpdatedAt string
}
type versionedConfigUpsertSpec[T any] struct {
entityName string
idKind string
loadTx func(context.Context, *sql.Tx) (T, error)
current func(T) versionedConfigCurrent
applyMetadata func(*T, versionedConfigMetadata)
writeTx func(context.Context, *sql.Tx, T) error
}
func upsertVersionedConfig[T any](ctx context.Context, s *Store, value T, changedBy string, spec versionedConfigUpsertSpec[T]) (T, error) {
var zero T
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return zero, fmt.Errorf("begin upsert %s: %w", spec.entityName, err)
}
defer tx.Rollback()
before, err := spec.loadTx(ctx, tx)
exists := err == nil
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return zero, err
}
before = zero
}
meta, err := s.nextVersionedConfigMetadata(spec.idKind, spec.current(before), exists)
if err != nil {
return zero, err
}
spec.applyMetadata(&value, meta)
if err := spec.writeTx(ctx, tx, value); err != nil {
return zero, fmt.Errorf("upsert %s: %w", spec.entityName, err)
}
after, err := spec.loadTx(ctx, tx)
if err != nil {
return zero, err
}
if err := tx.Commit(); err != nil {
return zero, fmt.Errorf("commit upsert %s: %w", spec.entityName, err)
}
return after, nil
}
func (s *Store) nextVersionedConfigMetadata(idKind string, current versionedConfigCurrent, exists bool) (versionedConfigMetadata, error) {
now := s.now()
meta := versionedConfigMetadata{
Action: "update",
ID: current.ID,
Version: current.Version + 1,
CreatedAt: current.CreatedAt,
UpdatedAt: now,
}
if exists {
return meta, nil
}
id, err := s.newID(idKind)
if err != nil {
return versionedConfigMetadata{}, err
}
meta.Action = "create"
meta.ID = id
meta.Version = 1
meta.CreatedAt = now
return meta, nil
}
type scanner interface {
Scan(dest ...any) error
}
func marshalJSONMapString(v map[string]string) (string, error) {
if len(v) == 0 {
return "{}", nil
}
data, err := json.Marshal(v)
if err != nil {
return "", fmt.Errorf("marshal json map string: %w", err)
}
return string(data), nil
}
func marshalJSONMapAny(v map[string]any) (string, error) {
if len(v) == 0 {
return "{}", nil
}
data, err := json.Marshal(v)
if err != nil {
return "", fmt.Errorf("marshal json map: %w", err)
}
return string(data), nil
}
func unmarshalJSON(src string, dst any) error {
if strings.TrimSpace(src) == "" {
src = "{}"
}
return json.Unmarshal([]byte(src), dst)
}
func nullableString(value string) any {
if value == "" {
return nil
}
return value
}
func coalesceString(value, fallback string) string {
if value != "" {
return value
}
return fallback
}
func boolToInt(value bool) int {
if value {
return 1
}
return 0
}