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 }