Projeto bot pontos
This commit is contained in:
125
backend/internal/adapters/postgres/store.go
Normal file
125
backend/internal/adapters/postgres/store.go
Normal file
@@ -0,0 +1,125 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user