161 lines
3.2 KiB
Go
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
|
|
}
|