Files
member-console/internal/web/dberrors.go
T
cgalo5758 88db730fcc Add dual licensing and SPDX headers
Introduce a commercial license option alongside AGPL-3.0-only, require a
CLA for contributors, and document the terms in COMMERCIAL.md and
NOTICE. Add a script to stamp SPDX headers on Go files and apply it
across the tree.
2026-09-06 02:29:42 -05:00

60 lines
2.0 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package web
import (
"errors"
"github.com/jackc/pgx/v5/pgconn"
)
// ConstraintMessages maps Postgres constraint names to the form field and
// message the operator should see when that constraint rejects a write.
// Handlers pass the constraints their form can plausibly hit; the names live
// next to the form they belong to (per docs/operator-ux-conventions.md §6)
// rather than in one global registry.
type ConstraintMessages map[string]FieldMessage
// FieldMessage is one field-level error: which form field to flag and what to say.
type FieldMessage struct {
Field string
Message string
}
// FieldErrorsFromDB translates a failed database write into field-level form
// errors, so constraint violations render as 422 + FieldErrors (the §6
// validation contract) instead of leaking raw driver text ("SQLSTATE 23505")
// into the UI. It returns ok=false when err is not a recognizable constraint
// violation — the caller should then treat it as a server error (log + generic
// message), never render err.Error().
//
// Resolution order: an exact constraint-name match in msgs wins; otherwise a
// generic per-SQLSTATE-class message lands on the form-level "" field.
func FieldErrorsFromDB(err error, msgs ConstraintMessages) (FieldErrors, bool) {
var pgErr *pgconn.PgError
if !errors.As(err, &pgErr) {
return nil, false
}
fe := New()
if fm, hit := msgs[pgErr.ConstraintName]; hit {
fe.Set(fm.Field, fm.Message)
return fe, true
}
switch pgErr.Code {
case "23505": // unique_violation
fe.Set("", "A record with these values already exists.")
case "23503": // foreign_key_violation
fe.Set("", "A referenced record no longer exists or is still in use.")
case "23514": // check_violation
fe.Set("", "This combination of values is not allowed.")
case "22001": // string_data_right_truncation
fe.Set("", "One of the values is too long.")
default:
return nil, false
}
return fe, true
}