view pkg/pgxutils/errors.go @ 5711:2dd155cc95ec revive-cleanup

Fix all revive issue (w/o machine generated stuff).
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Tue, 20 Feb 2024 22:22:57 +0100
parents 73563c4bba5b
children
line wrap: on
line source

// This is Free Software under GNU Affero General Public License v >= 3.0
// without warranty, see README.md and license for details.
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// License-Filename: LICENSES/AGPL-3.0.txt
//
// Copyright (C) 2019 by via donau
//   – Österreichische Wasserstraßen-Gesellschaft mbH
// Software engineering by Intevation GmbH
//
// Author(s):
//  * Tom Gottfried <tom.gottfried@intevation.de>
//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>

// Package pgxutils is used for error handling related to the pgx PostgreSQL driver.
package pgxutils

import (
	"net/http"
	"strings"

	"github.com/jackc/pgx"
)

const (
	// camel cased condition name = error code
	// from appendix A of PostgreSQL documentation
	notNullViolation      = "23502"
	foreignKeyViolation   = "23503"
	uniqueViolation       = "23505"
	checkViolation        = "23514"
	exclusionViolation    = "23P01"
	insufficientPrivilege = "42501"
	duplicateObject       = "42710"
	noDataFound           = "P0002"
)

// ReadableError wraps a given error Err and
// permits extraction of more user-friendly
// error messages from it in case it is an error
// from the PostgreSQL backend.
type ReadableError struct {
	Err error
}

func (re ReadableError) Error() string {
	m, _ := re.MessageAndCode()
	return m
}

// MessageAndCode returns a user-readable message
// and a matching HTTP status code.
// If its not a pgx.PgError it defaults to
// calling the parent Error method and returns its
// result together with http.StatusInternalServerError.
func (re ReadableError) MessageAndCode() (string, int) {
	if e, ok := re.Err.(pgx.PgError); ok {
		return messageAndCode(e)
	}
	return re.Err.Error(), http.StatusInternalServerError
}

func messageAndCode(err pgx.PgError) (m string, c int) {

	c = http.StatusInternalServerError

	// Most recent line from stacktrace contains failed statement
	recent := strings.SplitN(err.Where, "\n", 1)[0]

	switch err.Code {
	case notNullViolation:
		switch err.SchemaName {
		case "waterway":
			switch err.TableName {
			case "gauges":
				switch err.ColumnName {
				case "objname":
					m = "Missing objname"
					return
				case "geom":
					m = "Missing lat/lon"
					return
				case "zero_point":
					m = "Missing zeropoint"
					return
				}
			}
		}
	case foreignKeyViolation:
		switch err.SchemaName {
		case "waterway":
			switch err.TableName {
			case "gauge_measurements", "gauge_predictions", "bottlenecks":
				switch err.ConstraintName {
				case "waterway_bottlenecks_reference_gauge",
					"waterway_gauge_measurements_reference_gauge",
					"waterway_gauge_predictions_reference_gauge":
					m = "Referenced gauge with matching temporal validity not available"
					return
				}
			}
			switch err.TableName {
			case "fairway_marks_bcnlat_dirimps",
				"fairway_marks_daymar_dirimps",
				"fairway_marks_notmrk_dirimps":
				switch err.ConstraintName {
				case "fairway_marks_bcnlat_dirimps_dirimp_fkey",
					"fairway_marks_daymar_dirimps_dirimp_fkey",
					"fairway_marks_notmrk_dirimps_dirimp_fkey":
					m = "Invalid value for dirimp"
					return
				}
			}
		}
	case uniqueViolation:
		switch err.SchemaName {
		case "users":
			switch err.TableName {
			case "stretches":
				switch err.ConstraintName {
				case "stretches_name_staging_done_key":
					m = "A stretch with that name already exists"
					c = http.StatusConflict
					return
				}
			}
		case "waterway":
			switch err.TableName {
			case "sections":
				switch err.ConstraintName {
				case "sections_name_staging_done_key":
					m = "A section with that name already exists"
					c = http.StatusConflict
					return
				}
			case "fairway_dimensions":
				switch err.ConstraintName {
				case "fairway_dimensions_area_unique":
					m = "Duplicate fairway dimension area"
					c = http.StatusConflict
					return
				}
			}
		}
	case exclusionViolation:
		switch err.SchemaName {
		case "waterway":
			switch err.TableName {
			case "sections":
				switch err.ConstraintName {
				case "sections_name_country_excl":
					m = "A section with that name already exists for another country"
					c = http.StatusConflict
					return
				}
			}
		}
	case checkViolation:
		switch err.SchemaName {
		case "waterway":
			switch err.TableName {
			case "sounding_results":
				switch err.ConstraintName {
				case "b_sounding_results_in_bn_area":
					m = "Dataset does not intersect with given bottleneck"
					c = http.StatusConflict
					return
				}
			case "fairway_dimensions":
				switch err.ConstraintName {
				case "fairway_dimensions_area_check":
					m = "Geometry could not be stored as valid, non-empty polygon"
					return
				}
			}
		case "internal":
			switch err.TableName {
			case "user_profiles":
				switch err.ConstraintName {
				case "user_profiles_username_check":
					m = "User name too long"
					c = http.StatusBadRequest
					return
				}
			}
		}
	case duplicateObject:
		switch {
		case strings.Contains(recent, "CREATE ROLE"):
			m = "A user with that name already exists"
			c = http.StatusConflict
			return
		}
	case noDataFound:
		switch {
		case strings.Contains(recent, "isrsrange_points"):
			m = "No distance mark found for at least one given ISRS Location Code"
			return
		case strings.Contains(recent, "isrsrange_axis"):
			m = "No contiguous axis found between given ISRS Location Codes"
			return
		case strings.Contains(recent, "isrsrange_area"):
			m = "No area around axis between given ISRS Location Codes"
			return
		}
	case insufficientPrivilege:
		m = "Could not save: Data outside the area of responsibility."
		return
	}
	m = "Unexpected database error: " + err.Message
	return
}