axe/internal/repository/errors.go
2026-10-18 22:07:25 +03:00

113 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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...))
}