improve error handling

This commit is contained in:
Hayden 2022-09-24 11:14:36 -08:00
parent d7e589a10d
commit 29d7c277d3
10 changed files with 184 additions and 79 deletions

View file

@ -274,6 +274,15 @@ const docTemplate = `{
"schema": {
"$ref": "#/definitions/types.ItemOut"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/server.ValidationError"
}
}
}
}
}
@ -1025,6 +1034,17 @@ const docTemplate = `{
}
}
},
"server.ValidationError": {
"type": "object",
"properties": {
"field": {
"type": "string"
},
"reason": {
"type": "string"
}
}
},
"types.ApiSummary": {
"type": "object",
"properties": {

View file

@ -266,6 +266,15 @@
"schema": {
"$ref": "#/definitions/types.ItemOut"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/server.ValidationError"
}
}
}
}
}
@ -1017,6 +1026,17 @@
}
}
},
"server.ValidationError": {
"type": "object",
"properties": {
"field": {
"type": "string"
},
"reason": {
"type": "string"
}
}
},
"types.ApiSummary": {
"type": "object",
"properties": {

View file

@ -14,6 +14,13 @@ definitions:
items:
type: any
type: object
server.ValidationError:
properties:
field:
type: string
reason:
type: string
type: object
types.ApiSummary:
properties:
build:
@ -543,6 +550,12 @@ paths:
description: OK
schema:
$ref: '#/definitions/types.ItemOut'
"422":
description: Unprocessable Entity
schema:
items:
$ref: '#/definitions/server.ValidationError'
type: array
security:
- Bearer: []
summary: imports items into the database

View file

@ -43,7 +43,7 @@ func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux {
// API Version 1
v1Base := v1.BaseUrlFunc(prefix)
v1Ctrl := v1.NewControllerV1(a.services)
v1Ctrl := v1.NewControllerV1(a.services, v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize))
{
r.Get(v1Base("/status"), v1Ctrl.HandleBase(func() bool { return true }, types.Build{
Version: Version,

View file

@ -8,8 +8,15 @@ import (
"github.com/hay-kot/homebox/backend/pkgs/server"
)
func WithMaxUploadSize(maxUploadSize int64) func(*V1Controller) {
return func(ctrl *V1Controller) {
ctrl.maxUploadSize = maxUploadSize
}
}
type V1Controller struct {
svc *services.AllServices
svc *services.AllServices
maxUploadSize int64
}
func BaseUrlFunc(prefix string) func(s string) string {
@ -21,7 +28,7 @@ func BaseUrlFunc(prefix string) func(s string) string {
return prefixFunc
}
func NewControllerV1(svc *services.AllServices) *V1Controller {
func NewControllerV1(svc *services.AllServices, options ...func(*V1Controller)) *V1Controller {
ctrl := &V1Controller{
svc: svc,
}

View file

@ -2,10 +2,8 @@ package v1
import (
"encoding/csv"
"errors"
"net/http"
"github.com/hay-kot/homebox/backend/ent/attachment"
"github.com/hay-kot/homebox/backend/internal/services"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/pkgs/server"
@ -191,65 +189,3 @@ func (ctrl *V1Controller) HandleItemsImport() http.HandlerFunc {
server.Respond(w, http.StatusNoContent, nil)
}
}
// HandleItemsImport godocs
// @Summary imports items into the database
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Param file formData file true "File attachment"
// @Param type formData string true "Type of file"
// @Param name formData string true "name of the file including extension"
// @Success 200 {object} types.ItemOut
// @Router /v1/items/{id}/attachments [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Max upload size of 10 MB - TODO: Set via config
err := r.ParseMultipartForm(10 << 20)
if err != nil {
log.Err(err).Msg("failed to parse multipart form")
server.RespondServerError(w)
return
}
file, _, err := r.FormFile("file")
if err != nil {
log.Err(err).Msg("failed to get file from form")
server.RespondServerError(w)
return
}
attachmentName := r.FormValue("name")
if attachmentName == "" {
log.Err(err).Msg("failed to get name from form")
server.RespondError(w, http.StatusBadRequest, errors.New("name is required"))
}
attachmentType := r.FormValue("type")
if attachmentType == "" {
attachmentName = "attachment"
}
uid, _, err := ctrl.partialParseIdAndUser(w, r)
if err != nil {
return
}
ctx := services.NewContext(r.Context())
item, err := ctrl.svc.Items.AttachmentAdd(
ctx,
uid,
attachmentName,
attachment.Type(attachmentType),
file,
)
if err != nil {
log.Err(err).Msg("failed to add attachment")
server.RespondServerError(w)
return
}
server.Respond(w, http.StatusCreated, item)
}
}

View file

@ -1,18 +1,97 @@
package v1
import (
"errors"
"fmt"
"net/http"
"path/filepath"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent/attachment"
"github.com/hay-kot/homebox/backend/internal/services"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/rs/zerolog/log"
)
// HandleItemsImport godocs
// @Summary imports items into the database
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Param file formData file true "File attachment"
// @Param type formData string true "Type of file"
// @Param name formData string true "name of the file including extension"
// @Success 200 {object} types.ItemOut
// @Failure 422 {object} []server.ValidationError
// @Router /v1/items/{id}/attachments [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
if err != nil {
log.Err(err).Msg("failed to parse multipart form")
server.RespondError(w, http.StatusBadRequest, errors.New("failed to parse multipart form"))
return
}
errs := make(server.ValidationErrors, 0)
file, _, err := r.FormFile("file")
if err != nil {
switch {
case errors.Is(err, http.ErrMissingFile):
log.Debug().Msg("file for attachment is missing")
errs = errs.Append("file", "file is required")
default:
log.Err(err).Msg("failed to get file from form")
server.RespondServerError(w)
return
}
}
attachmentName := r.FormValue("name")
if attachmentName == "" {
log.Debug().Msg("failed to get name from form")
errs = errs.Append("name", "name is required")
}
if errs.HasErrors() {
server.Respond(w, http.StatusUnprocessableEntity, errs)
return
}
attachmentType := r.FormValue("type")
if attachmentType == "" {
attachmentType = attachment.TypeAttachment.String()
}
id, _, err := ctrl.partialParseIdAndUser(w, r)
if err != nil {
return
}
ctx := services.NewContext(r.Context())
item, err := ctrl.svc.Items.AttachmentAdd(
ctx,
id,
attachmentName,
attachment.Type(attachmentType),
file,
)
if err != nil {
log.Err(err).Msg("failed to add attachment")
server.RespondServerError(w)
return
}
server.Respond(w, http.StatusCreated, item)
}
}
// HandleItemAttachmentGet godocs
// @Summary retrieves an attachment for an item
// @Tags Items

View file

@ -58,7 +58,7 @@ func (svc *ItemService) AttachmentToken(ctx Context, itemId, attachmentId uuid.U
}
if _, err := os.Stat(attachment.Edges.Document.Path); os.IsNotExist(err) {
svc.AttachmentDelete(ctx, ctx.GID, itemId, attachmentId)
_ = svc.AttachmentDelete(ctx, ctx.GID, itemId, attachmentId)
return "", ErrNotFound
}

View file

@ -4,22 +4,47 @@ import (
"net/http"
)
type ValidationError struct {
Field string `json:"field"`
Reason string `json:"reason"`
}
type ValidationErrors []ValidationError
func (ve *ValidationErrors) HasErrors() bool {
if (ve == nil) || (len(*ve) == 0) {
return false
}
for _, err := range *ve {
if err.Field != "" {
return true
}
}
return false
}
func (ve ValidationErrors) Append(field, reasons string) ValidationErrors {
return append(ve, ValidationError{
Field: field,
Reason: reasons,
})
}
// ErrorBuilder is a helper type to build a response that contains an array of errors.
// Typical use cases are for returning an array of validation errors back to the user.
//
// Example:
//
//
// {
// "errors": [
// "invalid id",
// "invalid name",
// "invalid description"
// ],
// "message": "Unprocessable Entity",
// "status": 422
// }
//
// {
// "errors": [
// "invalid id",
// "invalid name",
// "invalid description"
// ],
// "message": "Unprocessable Entity",
// "status": 422
// }
type ErrorBuilder struct {
errs []string
}

View file

@ -21,6 +21,11 @@ export interface ServerResults {
items: any;
}
export interface ServerValidationError {
field: string;
reason: string;
}
export interface ApiSummary {
build: Build;
health: boolean;