axe/internal/repository/errors.go

113 lines
5.8 KiB
Go
Raw Normal View History

2026-10-18 19:07:25 +00:00
package repository
import (
"errors"
"fmt"
"strings"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
// Классы ошибок, которые понимают handlers.
//
// ErrNotFound / ErrConflict / ErrInvalid — ошибки предметной области,
// им можно отдавать клиенту код и сообщение.
// Всё остальное — инфраструктура (сеть, авторизация, падение БД):
// наружу уходит только «internal server error», детали пишутся в лог.
var (
// ErrNotFound — сущность не найдена → 404.
ErrNotFound = errors.New("not found")
// ErrConflict — нарушение уникальности или конфликт состояния → 409.
ErrConflict = errors.New("conflict")
// ErrInvalid — нарушение ограничений целостности (FK, CHECK, NOT NULL) → 422.
ErrInvalid = errors.New("invalid")
// Ниже — специализированные конфликты приглашений (миграция 0005).
// Каждый заворачивается в ErrConflict (→ 409), но хендлеру даёт
// точный текст для клиента: «уже состоит» и «ждём ответа» — разные
// ситуации, хотя обе это 409.
//
// ErrMembershipActive — мастер уже активно состоит в проекте.
ErrMembershipActive = errors.New("worker already active in project")
// ErrInvitationPending — приглашение отправлено и ещё не принято.
ErrInvitationPending = errors.New("invitation already pending")
// ErrInvitationExpired — срок ответа истёк, приглашение мертво.
ErrInvitationExpired = errors.New("invitation expired")
// ErrInvitationNotPending — строка есть, но ждать решения уже нечего
// (мастер ответил или строка устарела на момент операции).
ErrInvitationNotPending = errors.New("invitation is not pending")
// ErrResetToken — токен сброса пароля неизвестен, уже использован или
// истёк. Наружу уходит одним текстом (400 invalid token): различать
// причины — помощь переборщику.
ErrResetToken = errors.New("invalid reset token")
// ErrUserHasData — аккаунт нельзя удалить: на него ссылается история —
// заявки, проекты или участия (FK без каскада). Хендлер отвечает 409
// и предлагает отключить аккаунт вместо удаления — историю не стираем.
ErrUserHasData = errors.New("user has related records")
// ErrAlreadyConfirmed — подтверждать уже нечего: аккаунт подтверждён
// раньше (повторный POST /users/:id/confirm). Хендлер ветвится сам
// и отвечает 409 already confirmed.
ErrAlreadyConfirmed = errors.New("already confirmed")
// Ниже — специализированные конфликты фотоотчёта (миграция 0008).
// Оба заворачиваются в ErrConflict (→ 409), но хендлеру дают
// точный текст: «слишком много файлов» и «нельзя удалить» —
// разные ситуации, хотя обе это 409.
ErrTooManyPhotos = errors.New("too many photos")
// ErrPhotoNotRemovable — чужое фото или заявка уже закрыта.
ErrPhotoNotRemovable = errors.New("photo cannot be removed")
// Ниже — рубежи правила повторяющейся заявки: фоновый планировщик
// пропускает такое правило молча (создания заявки нет), а ручной
// запуск (POST /recurring/:id/run) получает конфликт с причиной.
// Каждый заворачивается в ErrConflict (→ 409).
ErrRuleNotActive = errors.New("rule is not active")
ErrRuleOpen = errors.New("open ticket exists")
ErrProjectNotActive = errors.New("project is not active")
ErrObjectNotActive = errors.New("object is not active")
)
// classify превращает ошибку pgx в ошибку одного из трёх классов.
// Инфраструктурные ошибки возвращаются как есть, чтобы handlers не
// приняли обрыв соединения за «запись не нашлась».
// Контекст вызывающего («ticket 12») сохраняется — он нужен для лога,
// наружу он не уходит: handlers отдают только фиксированные тексты.
func classify(err error) error {
if err == nil {
return nil
}
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
switch {
case pgErr.Code == pgUniqueViolation: // 23505
return fmt.Errorf("%w: %w", err, ErrConflict)
case strings.HasPrefix(pgErr.Code, "23"): // 23xxx — integrity constraint
return fmt.Errorf("%w: %w", err, ErrInvalid)
}
// Прочие коды (08xxx сеть, 40001 deadlock, 42xxx права) — инфраструктура.
return err
}
if errors.Is(err, pgx.ErrNoRows) {
return fmt.Errorf("%w: %w", err, ErrNotFound)
}
return err
}
const (
pgUniqueViolation = "23505"
pgFKViolation = "23503"
)
// notFound — обёртка, чтобы сообщение об ошибке было информативным в логе.
func notFound(format string, args ...any) error {
return fmt.Errorf("%w: %s", ErrNotFound, fmt.Sprintf(format, args...))
}