view pkg/pgxutils/errors.go @ 5591:0011f50cf216 surveysperbottleneckid

Removed no longer used alternative api for surveys/ endpoint. As bottlenecks in the summary for SR imports are now identified by their id and no longer by the (not guarantied to be unique!) name, there is no longer the need to request survey data by the name+date tuple (which isn't reliable anyway). So the workaround was now reversed.
author Sascha Wilde <wilde@sha-bang.de>
date Wed, 06 Apr 2022 13:30:29 +0200
parents 73563c4bba5b
children 2dd155cc95ec
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

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
}