Files
bot-pontos/backend/internal/adapters/postgres/store.go
2026-10-23 22:31:02 -03:00

126 lines
2.6 KiB
Go

package postgres
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
"botpontos/internal/domain"
)
// Store reúne as consultas da aplicação. As queries são escritas à mão com pgx.
//
// Convenções:
// - parâmetros de enum usam $n::text::<tipo>, para o pgx enviar texto;
// - datas (AAAA-MM-DD) trafegam como string, com $n::text::date e to_char na leitura;
// - filtros opcionais usam ($n::<tipo> IS NULL OR ...) com ponteiros nil.
type Store struct {
pool *pgxpool.Pool
}
func NewStore(pool *pgxpool.Pool) *Store {
return &Store{pool: pool}
}
// Ping é usado pelos healthchecks.
func (s *Store) Ping(ctx context.Context) error {
return s.pool.Ping(ctx)
}
type querier interface {
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}
type scanner interface {
Scan(dest ...any) error
}
// inTx executa fn numa transação; qualquer erro desfaz tudo.
func (s *Store) inTx(ctx context.Context, fn func(q querier) error) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer func() { _ = tx.Rollback(ctx) }()
if err := fn(tx); err != nil {
return err
}
return tx.Commit(ctx)
}
// mapErr traduz erros do pgx para os erros do domínio.
func mapErr(err error) error {
if err == nil {
return nil
}
if errors.Is(err, pgx.ErrNoRows) {
return domain.ErrNotFound
}
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
return domain.ErrConflict
}
return err
}
func expectOne(tag pgconn.CommandTag, err error) error {
if err != nil {
return mapErr(err)
}
if tag.RowsAffected() == 0 {
return domain.ErrNotFound
}
return nil
}
// collect lê todas as linhas com a função de leitura informada.
func collect[T any](rows pgx.Rows, err error, read func(scanner) (T, error)) ([]T, error) {
if err != nil {
return nil, err
}
defer rows.Close()
var out []T
for rows.Next() {
item, err := read(rows)
if err != nil {
return nil, err
}
out = append(out, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
func cols(prefix string, names ...string) string {
out := ""
for i, n := range names {
if i > 0 {
out += ", "
}
out += prefix + n
}
return out
}
func strPtr(s string) *string {
if s == "" {
return nil
}
return &s
}
func pauseReasonPtr(r *domain.PauseReason) *string {
if r == nil {
return nil
}
v := string(*r)
return &v
}