1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-03-20 06:21:06 +02:00
2024-09-29 21:09:46 +03:00

232 lines
6.0 KiB
Go

package router
import (
"database/sql"
"errors"
"io/fs"
"net/http"
"strings"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/pocketbase/tools/inflector"
)
// SafeErrorItem defines a common error interface for a printable public safe error.
type SafeErrorItem interface {
// Code represents a fixed unique identifier of the error (usually used as translation key).
Code() string
// Error is the default English human readable error message that will be returned.
Error() string
}
// SafeErrorParamsResolver defines an optional interface for specifying dynamic error parameters.
type SafeErrorParamsResolver interface {
// Params defines a map with dynamic parameters to return as part of the public safe error view.
Params() map[string]any
}
// SafeErrorResolver defines an error interface for resolving the public safe error fields.
type SafeErrorResolver interface {
// Resolve allows modifying and returning a new public safe error data map.
Resolve(errData map[string]any) any
}
// ApiError defines the struct for a basic api error response.
type ApiError struct {
rawData any
Data map[string]any `json:"data"`
Message string `json:"message"`
Status int `json:"status"`
}
// Error makes it compatible with the `error` interface.
func (e *ApiError) Error() string {
return e.Message
}
// RawData returns the unformatted error data (could be an internal error, text, etc.)
func (e *ApiError) RawData() any {
return e.rawData
}
// Is reports whether the current ApiError wraps the target.
func (e *ApiError) Is(target error) bool {
err, ok := e.rawData.(error)
if ok {
return errors.Is(err, target)
}
apiErr, ok := target.(*ApiError)
return ok && e == apiErr
}
// NewNotFoundError creates and returns 404 ApiError.
func NewNotFoundError(message string, rawErrData any) *ApiError {
if message == "" {
message = "The requested resource wasn't found."
}
return NewApiError(http.StatusNotFound, message, rawErrData)
}
// NewBadRequestError creates and returns 400 ApiError.
func NewBadRequestError(message string, rawErrData any) *ApiError {
if message == "" {
message = "Something went wrong while processing your request."
}
return NewApiError(http.StatusBadRequest, message, rawErrData)
}
// NewForbiddenError creates and returns 403 ApiError.
func NewForbiddenError(message string, rawErrData any) *ApiError {
if message == "" {
message = "You are not allowed to perform this request."
}
return NewApiError(http.StatusForbidden, message, rawErrData)
}
// NewUnauthorizedError creates and returns 401 ApiError.
func NewUnauthorizedError(message string, rawErrData any) *ApiError {
if message == "" {
message = "Missing or invalid authentication."
}
return NewApiError(http.StatusUnauthorized, message, rawErrData)
}
// NewInternalServerError creates and returns 500 ApiError.
func NewInternalServerError(message string, rawErrData any) *ApiError {
if message == "" {
message = "Something went wrong while processing your request."
}
return NewApiError(http.StatusInternalServerError, message, rawErrData)
}
func NewTooManyRequestsError(message string, rawErrData any) *ApiError {
if message == "" {
message = "Too Many Requests."
}
return NewApiError(http.StatusTooManyRequests, message, rawErrData)
}
// NewApiError creates and returns new normalized ApiError instance.
func NewApiError(status int, message string, rawErrData any) *ApiError {
if message == "" {
message = http.StatusText(status)
}
return &ApiError{
rawData: rawErrData,
Data: safeErrorsData(rawErrData),
Status: status,
Message: strings.TrimSpace(inflector.Sentenize(message)),
}
}
// ToApiError wraps err into ApiError instance (if not already).
func ToApiError(err error) *ApiError {
var apiErr *ApiError
if !errors.As(err, &apiErr) {
// no ApiError found -> assign a generic one
if errors.Is(err, sql.ErrNoRows) || errors.Is(err, fs.ErrNotExist) {
apiErr = NewNotFoundError("", err)
} else {
apiErr = NewBadRequestError("", err)
}
}
return apiErr
}
// -------------------------------------------------------------------
func safeErrorsData(data any) map[string]any {
switch v := data.(type) {
case validation.Errors:
return resolveSafeErrorsData(v)
case error:
validationErrors := validation.Errors{}
if errors.As(v, &validationErrors) {
return resolveSafeErrorsData(validationErrors)
}
return map[string]any{} // not nil to ensure that is json serialized as object
case map[string]validation.Error:
return resolveSafeErrorsData(v)
case map[string]SafeErrorItem:
return resolveSafeErrorsData(v)
case map[string]error:
return resolveSafeErrorsData(v)
case map[string]string:
return resolveSafeErrorsData(v)
case map[string]any:
return resolveSafeErrorsData(v)
default:
return map[string]any{} // not nil to ensure that is json serialized as object
}
}
func resolveSafeErrorsData[T any](data map[string]T) map[string]any {
result := map[string]any{}
for name, err := range data {
if isNestedError(err) {
result[name] = safeErrorsData(err)
} else {
result[name] = resolveSafeErrorItem(err)
}
}
return result
}
func isNestedError(err any) bool {
switch err.(type) {
case validation.Errors,
map[string]validation.Error,
map[string]SafeErrorItem,
map[string]error,
map[string]string,
map[string]any:
return true
}
return false
}
// resolveSafeErrorItem extracts from each validation error its
// public safe error code and message.
func resolveSafeErrorItem(err any) any {
data := map[string]any{}
if obj, ok := err.(SafeErrorItem); ok {
// extract the specific error code and message
data["code"] = obj.Code()
data["message"] = inflector.Sentenize(obj.Error())
} else {
// fallback to the default public safe values
data["code"] = "validation_invalid_value"
data["message"] = "Invalid value."
}
if s, ok := err.(SafeErrorParamsResolver); ok {
params := s.Params()
if len(params) > 0 {
data["params"] = params
}
}
if s, ok := err.(SafeErrorResolver); ok {
return s.Resolve(data)
}
return data
}