chore: refactor api endpoints (#339)

* move typegen code

* update taskfile to fix code-gen caches and use 'dir' attribute

* enable dumping stack traces for errors

* log request start and stop

* set zerolog stack handler

* fix routes function

* refactor context adapters to use requests directly

* change some method signatures to support GID

* start requiring validation tags

* first pass on updating handlers to use adapters

* add errs package

* code gen

* tidy

* rework API to use external server package
This commit is contained in:
Hayden 2023-03-20 20:32:10 -08:00 committed by GitHub
parent 184b494fc3
commit db80f8a159
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 806 additions and 1947 deletions

View file

@ -27,47 +27,47 @@ tasks:
--modular \ --modular \
--path ./backend/app/api/static/docs/swagger.json \ --path ./backend/app/api/static/docs/swagger.json \
--output ./frontend/lib/api/types --output ./frontend/lib/api/types
- go run ./scripts/process-types/*.go ./frontend/lib/api/types/data-contracts.ts - go run ./backend/app/tools/typegen/main.go ./frontend/lib/api/types/data-contracts.ts
- cp ./backend/app/api/static/docs/swagger.json docs/docs/api/openapi-2.0.json - cp ./backend/app/api/static/docs/swagger.json docs/docs/api/openapi-2.0.json
sources: sources:
- "./backend/app/api/**/*" - "./backend/app/api/**/*"
- "./backend/internal/data/**" - "./backend/internal/data/**"
- "./backend/internal/services/**/*" - "./backend/internal/core/services/**/*"
- "./scripts/process-types.py" - "./backend/app/tools/typegen/main.go"
generates:
- "./frontend/lib/api/types/data-contracts.ts"
- "./backend/internal/data/ent/schema"
- "./backend/app/api/static/docs/swagger.json"
- "./backend/app/api/static/docs/swagger.yaml"
go:run: go:run:
desc: Starts the backend api server (depends on generate task) desc: Starts the backend api server (depends on generate task)
dir: backend
deps: deps:
- generate - generate
cmds: cmds:
- cd backend && go run ./app/api/ {{ .CLI_ARGS }} - go run ./app/api/ {{ .CLI_ARGS }}
silent: false silent: false
go:test: go:test:
desc: Runs all go tests using gotestsum - supports passing gotestsum args desc: Runs all go tests using gotestsum - supports passing gotestsum args
dir: backend
cmds: cmds:
- cd backend && gotestsum {{ .CLI_ARGS }} ./... - gotestsum {{ .CLI_ARGS }} ./...
go:coverage: go:coverage:
desc: Runs all go tests with -race flag and generates a coverage report desc: Runs all go tests with -race flag and generates a coverage report
dir: backend
cmds: cmds:
- cd backend && go test -race -coverprofile=coverage.out -covermode=atomic ./app/... ./internal/... ./pkgs/... -v -cover - go test -race -coverprofile=coverage.out -covermode=atomic ./app/... ./internal/... ./pkgs/... -v -cover
silent: true silent: true
go:tidy: go:tidy:
desc: Runs go mod tidy on the backend desc: Runs go mod tidy on the backend
dir: backend
cmds: cmds:
- cd backend && go mod tidy - go mod tidy
go:lint: go:lint:
desc: Runs golangci-lint desc: Runs golangci-lint
dir: backend
cmds: cmds:
- cd backend && golangci-lint run ./... - golangci-lint run ./...
go:all: go:all:
desc: Runs all go test and lint related tasks desc: Runs all go test and lint related tasks
@ -78,19 +78,19 @@ tasks:
go:build: go:build:
desc: Builds the backend binary desc: Builds the backend binary
dir: backend
cmds: cmds:
- cd backend && go build -o ../build/backend ./app/api - go build -o ../build/backend ./app/api
db:generate: db:generate:
desc: Run Entgo.io Code Generation desc: Run Entgo.io Code Generation
dir: backend/internal/
cmds: cmds:
- | - |
cd backend/internal/ && go generate ./... \ go generate ./... \
--template=./data/ent/schema/templates/has_id.tmpl --template=./data/ent/schema/templates/has_id.tmpl
sources: sources:
- "./backend/internal/data/ent/schema/**/*" - "./backend/internal/data/ent/schema/**/*"
generates:
- "./backend/internal/ent/"
db:migration: db:migration:
desc: Runs the database diff engine to generate a SQL migration files desc: Runs the database diff engine to generate a SQL migration files
@ -101,23 +101,27 @@ tasks:
ui:watch: ui:watch:
desc: Starts the vitest test runner in watch mode desc: Starts the vitest test runner in watch mode
dir: frontend
cmds: cmds:
- cd frontend && pnpm run test:watch - pnpm run test:watch
ui:dev: ui:dev:
desc: Run frontend development server desc: Run frontend development server
dir: frontend
cmds: cmds:
- cd frontend && pnpm dev - pnpm dev
ui:fix: ui:fix:
desc: Runs prettier and eslint on the frontend desc: Runs prettier and eslint on the frontend
dir: frontend
cmds: cmds:
- cd frontend && pnpm run lint:fix - pnpm run lint:fix
ui:check: ui:check:
desc: Runs type checking desc: Runs type checking
dir: frontend
cmds: cmds:
- cd frontend && pnpm run typecheck - pnpm run typecheck
test:ci: test:ci:
desc: Runs end-to-end test on a live server (only for use in CI) desc: Runs end-to-end test on a live server (only for use in CI)

View file

@ -8,7 +8,7 @@ import (
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/config" "github.com/hay-kot/homebox/backend/internal/sys/config"
"github.com/hay-kot/homebox/backend/pkgs/mailer" "github.com/hay-kot/homebox/backend/pkgs/mailer"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/server"
) )
type app struct { type app struct {

View file

@ -5,9 +5,18 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
) )
type Wrapped struct {
Item interface{} `json:"item"`
}
func Wrap(v any) Wrapped {
return Wrapped{Item: v}
}
func WithMaxUploadSize(maxUploadSize int64) func(*V1Controller) { func WithMaxUploadSize(maxUploadSize int64) func(*V1Controller) {
return func(ctrl *V1Controller) { return func(ctrl *V1Controller) {
ctrl.maxUploadSize = maxUploadSize ctrl.maxUploadSize = maxUploadSize
@ -81,9 +90,9 @@ func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, options ..
// @Produce json // @Produce json
// @Success 200 {object} ApiSummary // @Success 200 {object} ApiSummary
// @Router /v1/status [GET] // @Router /v1/status [GET]
func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) server.HandlerFunc { func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
return server.Respond(w, http.StatusOK, ApiSummary{ return server.JSON(w, http.StatusOK, ApiSummary{
Healthy: ready(), Healthy: ready(),
Title: "Homebox", Title: "Homebox",
Message: "Track, Manage, and Organize your shit", Message: "Track, Manage, and Organize your shit",

View file

@ -7,7 +7,8 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -15,7 +16,7 @@ type ActionAmountResult struct {
Completed int `json:"completed"` Completed int `json:"completed"`
} }
func actionHandlerFactory(ref string, fn func(context.Context, uuid.UUID) (int, error)) server.HandlerFunc { func actionHandlerFactory(ref string, fn func(context.Context, uuid.UUID) (int, error)) errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context()) ctx := services.NewContext(r.Context())
@ -25,7 +26,7 @@ func actionHandlerFactory(ref string, fn func(context.Context, uuid.UUID) (int,
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.Respond(w, http.StatusOK, ActionAmountResult{Completed: totalCompleted}) return server.JSON(w, http.StatusOK, ActionAmountResult{Completed: totalCompleted})
} }
} }
@ -38,7 +39,7 @@ func actionHandlerFactory(ref string, fn func(context.Context, uuid.UUID) (int,
// @Success 200 {object} ActionAmountResult // @Success 200 {object} ActionAmountResult
// @Router /v1/actions/ensure-asset-ids [Post] // @Router /v1/actions/ensure-asset-ids [Post]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleEnsureAssetID() server.HandlerFunc { func (ctrl *V1Controller) HandleEnsureAssetID() errchain.HandlerFunc {
return actionHandlerFactory("ensure asset IDs", ctrl.svc.Items.EnsureAssetID) return actionHandlerFactory("ensure asset IDs", ctrl.svc.Items.EnsureAssetID)
} }
@ -51,7 +52,7 @@ func (ctrl *V1Controller) HandleEnsureAssetID() server.HandlerFunc {
// @Success 200 {object} ActionAmountResult // @Success 200 {object} ActionAmountResult
// @Router /v1/actions/ensure-import-refs [Post] // @Router /v1/actions/ensure-import-refs [Post]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleEnsureImportRefs() server.HandlerFunc { func (ctrl *V1Controller) HandleEnsureImportRefs() errchain.HandlerFunc {
return actionHandlerFactory("ensure import refs", ctrl.svc.Items.EnsureImportRef) return actionHandlerFactory("ensure import refs", ctrl.svc.Items.EnsureImportRef)
} }
@ -64,6 +65,6 @@ func (ctrl *V1Controller) HandleEnsureImportRefs() server.HandlerFunc {
// @Success 200 {object} ActionAmountResult // @Success 200 {object} ActionAmountResult
// @Router /v1/actions/zero-item-time-fields [Post] // @Router /v1/actions/zero-item-time-fields [Post]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemDateZeroOut() server.HandlerFunc { func (ctrl *V1Controller) HandleItemDateZeroOut() errchain.HandlerFunc {
return actionHandlerFactory("zero out date time", ctrl.repo.Items.ZeroOutTimeFields) return actionHandlerFactory("zero out date time", ctrl.repo.Items.ZeroOutTimeFields)
} }

View file

@ -9,7 +9,8 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -23,7 +24,7 @@ import (
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{} // @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
// @Router /v1/assets/{id} [GET] // @Router /v1/assets/{id} [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleAssetGet() server.HandlerFunc { func (ctrl *V1Controller) HandleAssetGet() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context()) ctx := services.NewContext(r.Context())
assetIdParam := chi.URLParam(r, "id") assetIdParam := chi.URLParam(r, "id")
@ -38,7 +39,7 @@ func (ctrl *V1Controller) HandleAssetGet() server.HandlerFunc {
if pageParam != "" { if pageParam != "" {
page, err = strconv.ParseInt(pageParam, 10, 64) page, err = strconv.ParseInt(pageParam, 10, 64)
if err != nil { if err != nil {
return server.Respond(w, http.StatusBadRequest, "Invalid page number") return server.JSON(w, http.StatusBadRequest, "Invalid page number")
} }
} }
@ -47,7 +48,7 @@ func (ctrl *V1Controller) HandleAssetGet() server.HandlerFunc {
if pageSizeParam != "" { if pageSizeParam != "" {
pageSize, err = strconv.ParseInt(pageSizeParam, 10, 64) pageSize, err = strconv.ParseInt(pageSizeParam, 10, 64)
if err != nil { if err != nil {
return server.Respond(w, http.StatusBadRequest, "Invalid page size") return server.JSON(w, http.StatusBadRequest, "Invalid page size")
} }
} }
@ -56,6 +57,6 @@ func (ctrl *V1Controller) HandleAssetGet() server.HandlerFunc {
log.Err(err).Msg("failed to get item") log.Err(err).Msg("failed to get item")
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.Respond(w, http.StatusOK, items) return server.JSON(w, http.StatusOK, items)
} }
} }

View file

@ -8,7 +8,8 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -36,26 +37,27 @@ type (
// @Produce json // @Produce json
// @Success 200 {object} TokenResponse // @Success 200 {object} TokenResponse
// @Router /v1/users/login [POST] // @Router /v1/users/login [POST]
func (ctrl *V1Controller) HandleAuthLogin() server.HandlerFunc { func (ctrl *V1Controller) HandleAuthLogin() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
loginForm := &LoginForm{} loginForm := &LoginForm{}
switch r.Header.Get("Content-Type") { switch r.Header.Get("Content-Type") {
case server.ContentFormUrlEncoded: case "application/x-www-form-urlencoded":
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
return server.Respond(w, http.StatusBadRequest, server.Wrap(err)) return errors.New("failed to parse form")
} }
loginForm.Username = r.PostFormValue("username") loginForm.Username = r.PostFormValue("username")
loginForm.Password = r.PostFormValue("password") loginForm.Password = r.PostFormValue("password")
case server.ContentJSON: case "application/json":
err := server.Decode(r, loginForm) err := server.Decode(r, loginForm)
if err != nil { if err != nil {
log.Err(err).Msg("failed to decode login form") log.Err(err).Msg("failed to decode login form")
return errors.New("failed to decode login form")
} }
default: default:
return server.Respond(w, http.StatusBadRequest, errors.New("invalid content type")) return server.JSON(w, http.StatusBadRequest, errors.New("invalid content type"))
} }
if loginForm.Username == "" || loginForm.Password == "" { if loginForm.Username == "" || loginForm.Password == "" {
@ -76,7 +78,7 @@ func (ctrl *V1Controller) HandleAuthLogin() server.HandlerFunc {
return validate.NewRequestError(errors.New("authentication failed"), http.StatusInternalServerError) return validate.NewRequestError(errors.New("authentication failed"), http.StatusInternalServerError)
} }
return server.Respond(w, http.StatusOK, TokenResponse{ return server.JSON(w, http.StatusOK, TokenResponse{
Token: "Bearer " + newToken.Raw, Token: "Bearer " + newToken.Raw,
ExpiresAt: newToken.ExpiresAt, ExpiresAt: newToken.ExpiresAt,
AttachmentToken: newToken.AttachmentToken, AttachmentToken: newToken.AttachmentToken,
@ -91,7 +93,7 @@ func (ctrl *V1Controller) HandleAuthLogin() server.HandlerFunc {
// @Success 204 // @Success 204
// @Router /v1/users/logout [POST] // @Router /v1/users/logout [POST]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleAuthLogout() server.HandlerFunc { func (ctrl *V1Controller) HandleAuthLogout() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
token := services.UseTokenCtx(r.Context()) token := services.UseTokenCtx(r.Context())
if token == "" { if token == "" {
@ -103,7 +105,7 @@ func (ctrl *V1Controller) HandleAuthLogout() server.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.Respond(w, http.StatusNoContent, nil) return server.JSON(w, http.StatusNoContent, nil)
} }
} }
@ -116,7 +118,7 @@ func (ctrl *V1Controller) HandleAuthLogout() server.HandlerFunc {
// @Success 200 // @Success 200
// @Router /v1/users/refresh [GET] // @Router /v1/users/refresh [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleAuthRefresh() server.HandlerFunc { func (ctrl *V1Controller) HandleAuthRefresh() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
requestToken := services.UseTokenCtx(r.Context()) requestToken := services.UseTokenCtx(r.Context())
if requestToken == "" { if requestToken == "" {
@ -128,6 +130,6 @@ func (ctrl *V1Controller) HandleAuthRefresh() server.HandlerFunc {
return validate.NewUnauthorizedError() return validate.NewUnauthorizedError()
} }
return server.Respond(w, http.StatusOK, newToken) return server.JSON(w, http.StatusOK, newToken)
} }
} }

View file

@ -6,14 +6,13 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
"github.com/rs/zerolog/log"
) )
type ( type (
GroupInvitationCreate struct { GroupInvitationCreate struct {
Uses int `json:"uses"` Uses int `json:"uses" validate:"required,min=1,max=100"`
ExpiresAt time.Time `json:"expiresAt"` ExpiresAt time.Time `json:"expiresAt"`
} }
@ -32,8 +31,13 @@ type (
// @Success 200 {object} repo.Group // @Success 200 {object} repo.Group
// @Router /v1/groups [Get] // @Router /v1/groups [Get]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGroupGet() server.HandlerFunc { func (ctrl *V1Controller) HandleGroupGet() errchain.HandlerFunc {
return ctrl.handleGroupGeneral() fn := func(r *http.Request) (repo.Group, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Groups.GroupByID(auth, auth.GID)
}
return adapters.Command(fn, http.StatusOK)
} }
// HandleGroupUpdate godoc // HandleGroupUpdate godoc
@ -45,41 +49,13 @@ func (ctrl *V1Controller) HandleGroupGet() server.HandlerFunc {
// @Success 200 {object} repo.Group // @Success 200 {object} repo.Group
// @Router /v1/groups [Put] // @Router /v1/groups [Put]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGroupUpdate() server.HandlerFunc { func (ctrl *V1Controller) HandleGroupUpdate() errchain.HandlerFunc {
return ctrl.handleGroupGeneral() fn := func(r *http.Request, body repo.GroupUpdate) (repo.Group, error) {
} auth := services.NewContext(r.Context())
return ctrl.svc.Group.UpdateGroup(auth, body)
func (ctrl *V1Controller) handleGroupGeneral() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context())
switch r.Method {
case http.MethodGet:
group, err := ctrl.repo.Groups.GroupByID(ctx, ctx.GID)
if err != nil {
log.Err(err).Msg("failed to get group")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, group)
case http.MethodPut:
data := repo.GroupUpdate{}
if err := server.Decode(r, &data); err != nil {
return validate.NewRequestError(err, http.StatusBadRequest)
}
group, err := ctrl.svc.Group.UpdateGroup(ctx, data)
if err != nil {
log.Err(err).Msg("failed to update group")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, group)
}
return nil
} }
return adapters.Action(fn, http.StatusOK)
} }
// HandleGroupInvitationsCreate godoc // HandleGroupInvitationsCreate godoc
@ -91,30 +67,22 @@ func (ctrl *V1Controller) handleGroupGeneral() server.HandlerFunc {
// @Success 200 {object} GroupInvitation // @Success 200 {object} GroupInvitation
// @Router /v1/groups/invitations [Post] // @Router /v1/groups/invitations [Post]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGroupInvitationsCreate() server.HandlerFunc { func (ctrl *V1Controller) HandleGroupInvitationsCreate() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { fn := func(r *http.Request, body GroupInvitationCreate) (GroupInvitation, error) {
data := GroupInvitationCreate{} if body.ExpiresAt.IsZero() {
if err := server.Decode(r, &data); err != nil { body.ExpiresAt = time.Now().Add(time.Hour * 24)
log.Err(err).Msg("failed to decode user registration data")
return validate.NewRequestError(err, http.StatusBadRequest)
} }
if data.ExpiresAt.IsZero() { auth := services.NewContext(r.Context())
data.ExpiresAt = time.Now().Add(time.Hour * 24)
}
ctx := services.NewContext(r.Context()) token, err := ctrl.svc.Group.NewInvitation(auth, body.Uses, body.ExpiresAt)
token, err := ctrl.svc.Group.NewInvitation(ctx, data.Uses, data.ExpiresAt) return GroupInvitation{
if err != nil {
log.Err(err).Msg("failed to create new token")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusCreated, GroupInvitation{
Token: token, Token: token,
ExpiresAt: data.ExpiresAt, ExpiresAt: body.ExpiresAt,
Uses: data.Uses, Uses: body.Uses,
}) }, err
} }
return adapters.Action(fn, http.StatusCreated)
} }

View file

@ -7,10 +7,13 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -27,7 +30,7 @@ import (
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{} // @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
// @Router /v1/items [GET] // @Router /v1/items [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemsGetAll() server.HandlerFunc { func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
extractQuery := func(r *http.Request) repo.ItemQuery { extractQuery := func(r *http.Request) repo.ItemQuery {
params := r.URL.Query() params := r.URL.Query()
@ -76,14 +79,14 @@ func (ctrl *V1Controller) HandleItemsGetAll() server.HandlerFunc {
items, err := ctrl.repo.Items.QueryByGroup(ctx, ctx.GID, extractQuery(r)) items, err := ctrl.repo.Items.QueryByGroup(ctx, ctx.GID, extractQuery(r))
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return server.Respond(w, http.StatusOK, repo.PaginationResult[repo.ItemSummary]{ return server.JSON(w, http.StatusOK, repo.PaginationResult[repo.ItemSummary]{
Items: []repo.ItemSummary{}, Items: []repo.ItemSummary{},
}) })
} }
log.Err(err).Msg("failed to get items") log.Err(err).Msg("failed to get items")
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.Respond(w, http.StatusOK, items) return server.JSON(w, http.StatusOK, items)
} }
} }
@ -93,26 +96,15 @@ func (ctrl *V1Controller) HandleItemsGetAll() server.HandlerFunc {
// @Tags Items // @Tags Items
// @Produce json // @Produce json
// @Param payload body repo.ItemCreate true "Item Data" // @Param payload body repo.ItemCreate true "Item Data"
// @Success 200 {object} repo.ItemSummary // @Success 201 {object} repo.ItemSummary
// @Router /v1/items [POST] // @Router /v1/items [POST]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemsCreate() server.HandlerFunc { func (ctrl *V1Controller) HandleItemsCreate() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { fn := func(r *http.Request, body repo.ItemCreate) (repo.ItemOut, error) {
createData := repo.ItemCreate{} return ctrl.svc.Items.Create(services.NewContext(r.Context()), body)
if err := server.Decode(r, &createData); err != nil {
log.Err(err).Msg("failed to decode request body")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
ctx := services.NewContext(r.Context())
item, err := ctrl.svc.Items.Create(ctx, createData)
if err != nil {
log.Err(err).Msg("failed to create item")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusCreated, item)
} }
return adapters.Action(fn, http.StatusCreated)
} }
// HandleItemGet godocs // HandleItemGet godocs
@ -124,8 +116,14 @@ func (ctrl *V1Controller) HandleItemsCreate() server.HandlerFunc {
// @Success 200 {object} repo.ItemOut // @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id} [GET] // @Router /v1/items/{id} [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemGet() server.HandlerFunc { func (ctrl *V1Controller) HandleItemGet() errchain.HandlerFunc {
return ctrl.handleItemsGeneral() fn := func(r *http.Request, ID uuid.UUID) (repo.ItemOut, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Items.GetOneByGroup(auth, auth.GID, ID)
}
return adapters.CommandID("id", fn, http.StatusOK)
} }
// HandleItemDelete godocs // HandleItemDelete godocs
@ -137,8 +135,14 @@ func (ctrl *V1Controller) HandleItemGet() server.HandlerFunc {
// @Success 204 // @Success 204
// @Router /v1/items/{id} [DELETE] // @Router /v1/items/{id} [DELETE]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemDelete() server.HandlerFunc { func (ctrl *V1Controller) HandleItemDelete() errchain.HandlerFunc {
return ctrl.handleItemsGeneral() fn := func(r *http.Request, ID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
err := ctrl.repo.Items.DeleteByGroup(auth, auth.GID, ID)
return nil, err
}
return adapters.CommandID("id", fn, http.StatusNoContent)
} }
// HandleItemUpdate godocs // HandleItemUpdate godocs
@ -151,50 +155,15 @@ func (ctrl *V1Controller) HandleItemDelete() server.HandlerFunc {
// @Success 200 {object} repo.ItemOut // @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id} [PUT] // @Router /v1/items/{id} [PUT]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemUpdate() server.HandlerFunc { func (ctrl *V1Controller) HandleItemUpdate() errchain.HandlerFunc {
return ctrl.handleItemsGeneral() fn := func(r *http.Request, ID uuid.UUID, body repo.ItemUpdate) (repo.ItemOut, error) {
} auth := services.NewContext(r.Context())
func (ctrl *V1Controller) handleItemsGeneral() server.HandlerFunc { body.ID = ID
return func(w http.ResponseWriter, r *http.Request) error { return ctrl.repo.Items.UpdateByGroup(auth, auth.GID, body)
ctx := services.NewContext(r.Context())
ID, err := ctrl.routeID(r)
if err != nil {
return err
}
switch r.Method {
case http.MethodGet:
items, err := ctrl.repo.Items.GetOneByGroup(r.Context(), ctx.GID, ID)
if err != nil {
log.Err(err).Msg("failed to get item")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, items)
case http.MethodDelete:
err = ctrl.repo.Items.DeleteByGroup(r.Context(), ctx.GID, ID)
if err != nil {
log.Err(err).Msg("failed to delete item")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusNoContent, nil)
case http.MethodPut:
body := repo.ItemUpdate{}
if err := server.Decode(r, &body); err != nil {
log.Err(err).Msg("failed to decode request body")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
body.ID = ID
result, err := ctrl.repo.Items.UpdateByGroup(r.Context(), ctx.GID, body)
if err != nil {
log.Err(err).Msg("failed to update item")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, result)
}
return nil
} }
return adapters.ActionID("id", fn, http.StatusOK)
} }
// HandleGetAllCustomFieldNames godocs // HandleGetAllCustomFieldNames godocs
@ -206,17 +175,13 @@ func (ctrl *V1Controller) handleItemsGeneral() server.HandlerFunc {
// @Router /v1/items/fields [GET] // @Router /v1/items/fields [GET]
// @Success 200 {object} []string // @Success 200 {object} []string
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGetAllCustomFieldNames() server.HandlerFunc { func (ctrl *V1Controller) HandleGetAllCustomFieldNames() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { fn := func(r *http.Request) ([]string, error) {
ctx := services.NewContext(r.Context()) auth := services.NewContext(r.Context())
return ctrl.repo.Items.GetAllCustomFieldNames(auth, auth.GID)
v, err := ctrl.repo.Items.GetAllCustomFieldNames(r.Context(), ctx.GID)
if err != nil {
return err
}
return server.Respond(w, http.StatusOK, v)
} }
return adapters.Command(fn, http.StatusOK)
} }
// HandleGetAllCustomFieldValues godocs // HandleGetAllCustomFieldValues godocs
@ -228,17 +193,18 @@ func (ctrl *V1Controller) HandleGetAllCustomFieldNames() server.HandlerFunc {
// @Router /v1/items/fields/values [GET] // @Router /v1/items/fields/values [GET]
// @Success 200 {object} []string // @Success 200 {object} []string
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGetAllCustomFieldValues() server.HandlerFunc { func (ctrl *V1Controller) HandleGetAllCustomFieldValues() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { type query struct {
ctx := services.NewContext(r.Context()) Field string `schema:"field" validate:"required"`
v, err := ctrl.repo.Items.GetAllCustomFieldValues(r.Context(), ctx.GID, r.URL.Query().Get("field"))
if err != nil {
return err
}
return server.Respond(w, http.StatusOK, v)
} }
fn := func(r *http.Request, q query) ([]string, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Items.GetAllCustomFieldValues(auth, auth.GID, q.Field)
}
return adapters.Action(fn, http.StatusOK)
} }
// HandleItemsImport godocs // HandleItemsImport godocs
@ -250,7 +216,7 @@ func (ctrl *V1Controller) HandleGetAllCustomFieldValues() server.HandlerFunc {
// @Param csv formData file true "Image to upload" // @Param csv formData file true "Image to upload"
// @Router /v1/items/import [Post] // @Router /v1/items/import [Post]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemsImport() server.HandlerFunc { func (ctrl *V1Controller) HandleItemsImport() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20) err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
if err != nil { if err != nil {
@ -272,7 +238,7 @@ func (ctrl *V1Controller) HandleItemsImport() server.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.Respond(w, http.StatusNoContent, nil) return server.JSON(w, http.StatusNoContent, nil)
} }
} }
@ -283,7 +249,7 @@ func (ctrl *V1Controller) HandleItemsImport() server.HandlerFunc {
// @Success 200 {string} string "text/csv" // @Success 200 {string} string "text/csv"
// @Router /v1/items/export [GET] // @Router /v1/items/export [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemsExport() server.HandlerFunc { func (ctrl *V1Controller) HandleItemsExport() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context()) ctx := services.NewContext(r.Context())

View file

@ -8,7 +8,8 @@ import (
"github.com/hay-kot/homebox/backend/internal/data/ent/attachment" "github.com/hay-kot/homebox/backend/internal/data/ent/attachment"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -28,10 +29,10 @@ type (
// @Param type formData string true "Type of file" // @Param type formData string true "Type of file"
// @Param name formData string true "name of the file including extension" // @Param name formData string true "name of the file including extension"
// @Success 200 {object} repo.ItemOut // @Success 200 {object} repo.ItemOut
// @Failure 422 {object} server.ErrorResponse // @Failure 422 {object} mid.ErrorResponse
// @Router /v1/items/{id}/attachments [POST] // @Router /v1/items/{id}/attachments [POST]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentCreate() server.HandlerFunc { func (ctrl *V1Controller) HandleItemAttachmentCreate() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20) err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
if err != nil { if err != nil {
@ -61,7 +62,7 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() server.HandlerFunc {
} }
if !errs.Nil() { if !errs.Nil() {
return server.Respond(w, http.StatusUnprocessableEntity, errs) return server.JSON(w, http.StatusUnprocessableEntity, errs)
} }
attachmentType := r.FormValue("type") attachmentType := r.FormValue("type")
@ -88,7 +89,7 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() server.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.Respond(w, http.StatusCreated, item) return server.JSON(w, http.StatusCreated, item)
} }
} }
@ -102,7 +103,7 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() server.HandlerFunc {
// @Success 200 {object} ItemAttachmentToken // @Success 200 {object} ItemAttachmentToken
// @Router /v1/items/{id}/attachments/{attachment_id} [GET] // @Router /v1/items/{id}/attachments/{attachment_id} [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentGet() server.HandlerFunc { func (ctrl *V1Controller) HandleItemAttachmentGet() errchain.HandlerFunc {
return ctrl.handleItemAttachmentsHandler return ctrl.handleItemAttachmentsHandler
} }
@ -115,7 +116,7 @@ func (ctrl *V1Controller) HandleItemAttachmentGet() server.HandlerFunc {
// @Success 204 // @Success 204
// @Router /v1/items/{id}/attachments/{attachment_id} [DELETE] // @Router /v1/items/{id}/attachments/{attachment_id} [DELETE]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentDelete() server.HandlerFunc { func (ctrl *V1Controller) HandleItemAttachmentDelete() errchain.HandlerFunc {
return ctrl.handleItemAttachmentsHandler return ctrl.handleItemAttachmentsHandler
} }
@ -129,7 +130,7 @@ func (ctrl *V1Controller) HandleItemAttachmentDelete() server.HandlerFunc {
// @Success 200 {object} repo.ItemOut // @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id}/attachments/{attachment_id} [PUT] // @Router /v1/items/{id}/attachments/{attachment_id} [PUT]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentUpdate() server.HandlerFunc { func (ctrl *V1Controller) HandleItemAttachmentUpdate() errchain.HandlerFunc {
return ctrl.handleItemAttachmentsHandler return ctrl.handleItemAttachmentsHandler
} }
@ -164,7 +165,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.Respond(w, http.StatusNoContent, nil) return server.JSON(w, http.StatusNoContent, nil)
// Update Attachment Handler // Update Attachment Handler
case http.MethodPut: case http.MethodPut:
@ -182,7 +183,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.Respond(w, http.StatusOK, val) return server.JSON(w, http.StatusOK, val)
} }
return nil return nil

View file

@ -3,12 +3,11 @@ package v1
import ( import (
"net/http" "net/http"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
"github.com/rs/zerolog/log"
) )
// HandleLabelsGetAll godoc // HandleLabelsGetAll godoc
@ -16,19 +15,16 @@ import (
// @Summary Get All Labels // @Summary Get All Labels
// @Tags Labels // @Tags Labels
// @Produce json // @Produce json
// @Success 200 {object} server.Results{items=[]repo.LabelOut} // @Success 200 {object} Wrapped{items=[]repo.LabelOut}
// @Router /v1/labels [GET] // @Router /v1/labels [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLabelsGetAll() server.HandlerFunc { func (ctrl *V1Controller) HandleLabelsGetAll() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { fn := func(r *http.Request) ([]repo.LabelSummary, error) {
user := services.UseUserCtx(r.Context()) auth := services.NewContext(r.Context())
labels, err := ctrl.repo.Labels.GetAll(r.Context(), user.GroupID) return ctrl.repo.Labels.GetAll(auth, auth.GID)
if err != nil {
log.Err(err).Msg("error getting labels")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, server.Results{Items: labels})
} }
return adapters.Command(fn, http.StatusOK)
} }
// HandleLabelsCreate godoc // HandleLabelsCreate godoc
@ -40,23 +36,13 @@ func (ctrl *V1Controller) HandleLabelsGetAll() server.HandlerFunc {
// @Success 200 {object} repo.LabelSummary // @Success 200 {object} repo.LabelSummary
// @Router /v1/labels [POST] // @Router /v1/labels [POST]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLabelsCreate() server.HandlerFunc { func (ctrl *V1Controller) HandleLabelsCreate() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { fn := func(r *http.Request, data repo.LabelCreate) (repo.LabelOut, error) {
createData := repo.LabelCreate{} auth := services.NewContext(r.Context())
if err := server.Decode(r, &createData); err != nil { return ctrl.repo.Labels.Create(auth, auth.GID, data)
log.Err(err).Msg("error decoding label create data")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
user := services.UseUserCtx(r.Context())
label, err := ctrl.repo.Labels.Create(r.Context(), user.GroupID, createData)
if err != nil {
log.Err(err).Msg("error creating label")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusCreated, label)
} }
return adapters.Action(fn, http.StatusCreated)
} }
// HandleLabelDelete godocs // HandleLabelDelete godocs
@ -68,8 +54,14 @@ func (ctrl *V1Controller) HandleLabelsCreate() server.HandlerFunc {
// @Success 204 // @Success 204
// @Router /v1/labels/{id} [DELETE] // @Router /v1/labels/{id} [DELETE]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLabelDelete() server.HandlerFunc { func (ctrl *V1Controller) HandleLabelDelete() errchain.HandlerFunc {
return ctrl.handleLabelsGeneral() fn := func(r *http.Request, ID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
err := ctrl.repo.Labels.DeleteByGroup(auth, auth.GID, ID)
return nil, err
}
return adapters.CommandID("id", fn, http.StatusNoContent)
} }
// HandleLabelGet godocs // HandleLabelGet godocs
@ -81,8 +73,13 @@ func (ctrl *V1Controller) HandleLabelDelete() server.HandlerFunc {
// @Success 200 {object} repo.LabelOut // @Success 200 {object} repo.LabelOut
// @Router /v1/labels/{id} [GET] // @Router /v1/labels/{id} [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLabelGet() server.HandlerFunc { func (ctrl *V1Controller) HandleLabelGet() errchain.HandlerFunc {
return ctrl.handleLabelsGeneral() fn := func(r *http.Request, ID uuid.UUID) (repo.LabelOut, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Labels.GetOneByGroup(auth, auth.GID, ID)
}
return adapters.CommandID("id", fn, http.StatusOK)
} }
// HandleLabelUpdate godocs // HandleLabelUpdate godocs
@ -94,55 +91,12 @@ func (ctrl *V1Controller) HandleLabelGet() server.HandlerFunc {
// @Success 200 {object} repo.LabelOut // @Success 200 {object} repo.LabelOut
// @Router /v1/labels/{id} [PUT] // @Router /v1/labels/{id} [PUT]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLabelUpdate() server.HandlerFunc { func (ctrl *V1Controller) HandleLabelUpdate() errchain.HandlerFunc {
return ctrl.handleLabelsGeneral() fn := func(r *http.Request, ID uuid.UUID, data repo.LabelUpdate) (repo.LabelOut, error) {
} auth := services.NewContext(r.Context())
data.ID = ID
func (ctrl *V1Controller) handleLabelsGeneral() server.HandlerFunc { return ctrl.repo.Labels.UpdateByGroup(auth, auth.GID, data)
return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context())
ID, err := ctrl.routeID(r)
if err != nil {
return err
}
switch r.Method {
case http.MethodGet:
labels, err := ctrl.repo.Labels.GetOneByGroup(r.Context(), ctx.GID, ID)
if err != nil {
if ent.IsNotFound(err) {
log.Err(err).
Str("id", ID.String()).
Msg("label not found")
return validate.NewRequestError(err, http.StatusNotFound)
}
log.Err(err).Msg("error getting label")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, labels)
case http.MethodDelete:
err = ctrl.repo.Labels.DeleteByGroup(ctx, ctx.GID, ID)
if err != nil {
log.Err(err).Msg("error deleting label")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusNoContent, nil)
case http.MethodPut:
body := repo.LabelUpdate{}
if err := server.Decode(r, &body); err != nil {
return validate.NewRequestError(err, http.StatusInternalServerError)
}
body.ID = ID
result, err := ctrl.repo.Labels.UpdateByGroup(ctx, ctx.GID, body)
if err != nil {
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, result)
}
return nil
} }
return adapters.ActionID("id", fn, http.StatusOK)
} }

View file

@ -3,77 +3,50 @@ package v1
import ( import (
"net/http" "net/http"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
"github.com/rs/zerolog/log"
) )
// HandleLocationTreeQuery godoc // HandleLocationTreeQuery
// //
// @Summary Get Locations Tree // @Summary Get Locations Tree
// @Tags Locations // @Tags Locations
// @Produce json // @Produce json
// @Param withItems query bool false "include items in response tree" // @Param withItems query bool false "include items in response tree"
// @Success 200 {object} server.Results{items=[]repo.TreeItem} // @Success 200 {object} Wrapped{items=[]repo.TreeItem}
// @Router /v1/locations/tree [GET] // @Router /v1/locations/tree [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLocationTreeQuery() server.HandlerFunc { func (ctrl *V1Controller) HandleLocationTreeQuery() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { fn := func(r *http.Request, query repo.TreeQuery) ([]repo.TreeItem, error) {
user := services.UseUserCtx(r.Context()) auth := services.NewContext(r.Context())
return ctrl.repo.Locations.Tree(auth, auth.GID, query)
q := r.URL.Query()
withItems := queryBool(q.Get("withItems"))
locTree, err := ctrl.repo.Locations.Tree(
r.Context(),
user.GroupID,
repo.TreeQuery{
WithItems: withItems,
},
)
if err != nil {
log.Err(err).Msg("failed to get locations tree")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, server.Results{Items: locTree})
} }
return adapters.Query(fn, http.StatusOK)
} }
// HandleLocationGetAll godoc // HandleLocationGetAll
// //
// @Summary Get All Locations // @Summary Get All Locations
// @Tags Locations // @Tags Locations
// @Produce json // @Produce json
// @Param filterChildren query bool false "Filter locations with parents" // @Param filterChildren query bool false "Filter locations with parents"
// @Success 200 {object} server.Results{items=[]repo.LocationOutCount} // @Success 200 {object} Wrapped{items=[]repo.LocationOutCount}
// @Router /v1/locations [GET] // @Router /v1/locations [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLocationGetAll() server.HandlerFunc { func (ctrl *V1Controller) HandleLocationGetAll() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { fn := func(r *http.Request, q repo.LocationQuery) ([]repo.LocationOutCount, error) {
user := services.UseUserCtx(r.Context()) auth := services.NewContext(r.Context())
return ctrl.repo.Locations.GetAll(auth, auth.GID, q)
q := r.URL.Query()
filter := repo.LocationQuery{
FilterChildren: queryBool(q.Get("filterChildren")),
}
locations, err := ctrl.repo.Locations.GetAll(r.Context(), user.GroupID, filter)
if err != nil {
log.Err(err).Msg("failed to get locations")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, server.Results{Items: locations})
} }
return adapters.Query(fn, http.StatusOK)
} }
// HandleLocationCreate godoc // HandleLocationCreate
// //
// @Summary Create Location // @Summary Create Location
// @Tags Locations // @Tags Locations
@ -82,26 +55,16 @@ func (ctrl *V1Controller) HandleLocationGetAll() server.HandlerFunc {
// @Success 200 {object} repo.LocationSummary // @Success 200 {object} repo.LocationSummary
// @Router /v1/locations [POST] // @Router /v1/locations [POST]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLocationCreate() server.HandlerFunc { func (ctrl *V1Controller) HandleLocationCreate() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { fn := func(r *http.Request, createData repo.LocationCreate) (repo.LocationOut, error) {
createData := repo.LocationCreate{} auth := services.NewContext(r.Context())
if err := server.Decode(r, &createData); err != nil { return ctrl.repo.Locations.Create(auth, auth.GID, createData)
log.Err(err).Msg("failed to decode location create data")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
user := services.UseUserCtx(r.Context())
location, err := ctrl.repo.Locations.Create(r.Context(), user.GroupID, createData)
if err != nil {
log.Err(err).Msg("failed to create location")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusCreated, location)
} }
return adapters.Action(fn, http.StatusCreated)
} }
// HandleLocationDelete godocs // HandleLocationDelete
// //
// @Summary Delete Location // @Summary Delete Location
// @Tags Locations // @Tags Locations
@ -110,11 +73,17 @@ func (ctrl *V1Controller) HandleLocationCreate() server.HandlerFunc {
// @Success 204 // @Success 204
// @Router /v1/locations/{id} [DELETE] // @Router /v1/locations/{id} [DELETE]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLocationDelete() server.HandlerFunc { func (ctrl *V1Controller) HandleLocationDelete() errchain.HandlerFunc {
return ctrl.handleLocationGeneral() fn := func(r *http.Request, ID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
err := ctrl.repo.Locations.DeleteByGroup(auth, auth.GID, ID)
return nil, err
}
return adapters.CommandID("id", fn, http.StatusNoContent)
} }
// HandleLocationGet godocs // HandleLocationGet
// //
// @Summary Get Location // @Summary Get Location
// @Tags Locations // @Tags Locations
@ -123,11 +92,16 @@ func (ctrl *V1Controller) HandleLocationDelete() server.HandlerFunc {
// @Success 200 {object} repo.LocationOut // @Success 200 {object} repo.LocationOut
// @Router /v1/locations/{id} [GET] // @Router /v1/locations/{id} [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLocationGet() server.HandlerFunc { func (ctrl *V1Controller) HandleLocationGet() errchain.HandlerFunc {
return ctrl.handleLocationGeneral() fn := func(r *http.Request, ID uuid.UUID) (repo.LocationOut, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Locations.GetOneByGroup(auth, auth.GID, ID)
}
return adapters.CommandID("id", fn, http.StatusOK)
} }
// HandleLocationUpdate godocs // HandleLocationUpdate
// //
// @Summary Update Location // @Summary Update Location
// @Tags Locations // @Tags Locations
@ -137,58 +111,12 @@ func (ctrl *V1Controller) HandleLocationGet() server.HandlerFunc {
// @Success 200 {object} repo.LocationOut // @Success 200 {object} repo.LocationOut
// @Router /v1/locations/{id} [PUT] // @Router /v1/locations/{id} [PUT]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLocationUpdate() server.HandlerFunc { func (ctrl *V1Controller) HandleLocationUpdate() errchain.HandlerFunc {
return ctrl.handleLocationGeneral() fn := func(r *http.Request, ID uuid.UUID, body repo.LocationUpdate) (repo.LocationOut, error) {
} auth := services.NewContext(r.Context())
body.ID = ID
func (ctrl *V1Controller) handleLocationGeneral() server.HandlerFunc { return ctrl.repo.Locations.UpdateByGroup(auth, auth.GID, ID, body)
return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context())
ID, err := ctrl.routeID(r)
if err != nil {
return err
}
switch r.Method {
case http.MethodGet:
location, err := ctrl.repo.Locations.GetOneByGroup(r.Context(), ctx.GID, ID)
if err != nil {
l := log.Err(err).
Str("ID", ID.String()).
Str("GID", ctx.GID.String())
if ent.IsNotFound(err) {
l.Msg("location not found")
return validate.NewRequestError(err, http.StatusNotFound)
}
l.Msg("failed to get location")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, location)
case http.MethodPut:
body := repo.LocationUpdate{}
if err := server.Decode(r, &body); err != nil {
log.Err(err).Msg("failed to decode location update data")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
body.ID = ID
result, err := ctrl.repo.Locations.UpdateOneByGroup(r.Context(), ctx.GID, ID, body)
if err != nil {
log.Err(err).Msg("failed to update location")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, result)
case http.MethodDelete:
err = ctrl.repo.Locations.DeleteByGroup(r.Context(), ctx.GID, ID)
if err != nil {
log.Err(err).Msg("failed to delete location")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusNoContent, nil)
}
return nil
} }
return adapters.ActionID("id", fn, http.StatusOK)
} }

View file

@ -2,13 +2,12 @@ package v1
import ( import (
"net/http" "net/http"
"strconv"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
"github.com/rs/zerolog/log"
) )
// HandleMaintenanceGetLog godoc // HandleMaintenanceGetLog godoc
@ -19,8 +18,13 @@ import (
// @Success 200 {object} repo.MaintenanceLog // @Success 200 {object} repo.MaintenanceLog
// @Router /v1/items/{id}/maintenance [GET] // @Router /v1/items/{id}/maintenance [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceLogGet() server.HandlerFunc { func (ctrl *V1Controller) HandleMaintenanceLogGet() errchain.HandlerFunc {
return ctrl.handleMaintenanceLog() fn := func(r *http.Request, ID uuid.UUID, q repo.MaintenanceLogQuery) (repo.MaintenanceLog, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.MaintEntry.GetLog(auth, auth.GID, ID, q)
}
return adapters.QueryID("id", fn, http.StatusOK)
} }
// HandleMaintenanceEntryCreate godoc // HandleMaintenanceEntryCreate godoc
@ -29,11 +33,16 @@ func (ctrl *V1Controller) HandleMaintenanceLogGet() server.HandlerFunc {
// @Tags Maintenance // @Tags Maintenance
// @Produce json // @Produce json
// @Param payload body repo.MaintenanceEntryCreate true "Entry Data" // @Param payload body repo.MaintenanceEntryCreate true "Entry Data"
// @Success 200 {object} repo.MaintenanceEntry // @Success 201 {object} repo.MaintenanceEntry
// @Router /v1/items/{id}/maintenance [POST] // @Router /v1/items/{id}/maintenance [POST]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceEntryCreate() server.HandlerFunc { func (ctrl *V1Controller) HandleMaintenanceEntryCreate() errchain.HandlerFunc {
return ctrl.handleMaintenanceLog() fn := func(r *http.Request, itemID uuid.UUID, body repo.MaintenanceEntryCreate) (repo.MaintenanceEntry, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.MaintEntry.Create(auth, itemID, body)
}
return adapters.ActionID("id", fn, http.StatusCreated)
} }
// HandleMaintenanceEntryDelete godoc // HandleMaintenanceEntryDelete godoc
@ -44,8 +53,14 @@ func (ctrl *V1Controller) HandleMaintenanceEntryCreate() server.HandlerFunc {
// @Success 204 // @Success 204
// @Router /v1/items/{id}/maintenance/{entry_id} [DELETE] // @Router /v1/items/{id}/maintenance/{entry_id} [DELETE]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceEntryDelete() server.HandlerFunc { func (ctrl *V1Controller) HandleMaintenanceEntryDelete() errchain.HandlerFunc {
return ctrl.handleMaintenanceLog() fn := func(r *http.Request, entryID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
err := ctrl.repo.MaintEntry.Delete(auth, entryID)
return nil, err
}
return adapters.CommandID("entry_id", fn, http.StatusNoContent)
} }
// HandleMaintenanceEntryUpdate godoc // HandleMaintenanceEntryUpdate godoc
@ -57,81 +72,11 @@ func (ctrl *V1Controller) HandleMaintenanceEntryDelete() server.HandlerFunc {
// @Success 200 {object} repo.MaintenanceEntry // @Success 200 {object} repo.MaintenanceEntry
// @Router /v1/items/{id}/maintenance/{entry_id} [PUT] // @Router /v1/items/{id}/maintenance/{entry_id} [PUT]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceEntryUpdate() server.HandlerFunc { func (ctrl *V1Controller) HandleMaintenanceEntryUpdate() errchain.HandlerFunc {
return ctrl.handleMaintenanceLog() fn := func(r *http.Request, entryID uuid.UUID, body repo.MaintenanceEntryUpdate) (repo.MaintenanceEntry, error) {
} auth := services.NewContext(r.Context())
return ctrl.repo.MaintEntry.Update(auth, entryID, body)
func (ctrl *V1Controller) handleMaintenanceLog() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context())
itemID, err := ctrl.routeID(r)
if err != nil {
return err
}
switch r.Method {
case http.MethodGet:
completed, _ := strconv.ParseBool(r.URL.Query().Get("completed"))
scheduled, _ := strconv.ParseBool(r.URL.Query().Get("scheduled"))
query := repo.MaintenanceLogQuery{
Completed: completed,
Scheduled: scheduled,
}
mlog, err := ctrl.repo.MaintEntry.GetLog(ctx, itemID, query)
if err != nil {
log.Err(err).Msg("failed to get items")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, mlog)
case http.MethodPost:
var create repo.MaintenanceEntryCreate
err := server.Decode(r, &create)
if err != nil {
return validate.NewRequestError(err, http.StatusBadRequest)
}
entry, err := ctrl.repo.MaintEntry.Create(ctx, itemID, create)
if err != nil {
log.Err(err).Msg("failed to create item")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusCreated, entry)
case http.MethodPut:
entryID, err := ctrl.routeUUID(r, "entry_id")
if err != nil {
return err
}
var update repo.MaintenanceEntryUpdate
err = server.Decode(r, &update)
if err != nil {
return validate.NewRequestError(err, http.StatusBadRequest)
}
entry, err := ctrl.repo.MaintEntry.Update(ctx, entryID, update)
if err != nil {
log.Err(err).Msg("failed to update item")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, entry)
case http.MethodDelete:
entryID, err := ctrl.routeUUID(r, "entry_id")
if err != nil {
return err
}
err = ctrl.repo.MaintEntry.Delete(ctx, entryID)
if err != nil {
log.Err(err).Msg("failed to delete item")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusNoContent, nil)
}
return nil
} }
return adapters.ActionID("entry_id", fn, http.StatusOK)
} }

View file

@ -1,7 +1,6 @@
package v1 package v1
import ( import (
"context"
"net/http" "net/http"
"github.com/containrrr/shoutrrr" "github.com/containrrr/shoutrrr"
@ -9,7 +8,7 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/web/adapters" "github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
) )
// HandleGetUserNotifiers godoc // HandleGetUserNotifiers godoc
@ -17,13 +16,13 @@ import (
// @Summary Get Notifiers // @Summary Get Notifiers
// @Tags Notifiers // @Tags Notifiers
// @Produce json // @Produce json
// @Success 200 {object} server.Results{items=[]repo.NotifierOut} // @Success 200 {object} Wrapped{items=[]repo.NotifierOut}
// @Router /v1/notifiers [GET] // @Router /v1/notifiers [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGetUserNotifiers() server.HandlerFunc { func (ctrl *V1Controller) HandleGetUserNotifiers() errchain.HandlerFunc {
fn := func(ctx context.Context, _ struct{}) ([]repo.NotifierOut, error) { fn := func(r *http.Request, _ struct{}) ([]repo.NotifierOut, error) {
user := services.UseUserCtx(ctx) user := services.UseUserCtx(r.Context())
return ctrl.repo.Notifiers.GetByUser(ctx, user.ID) return ctrl.repo.Notifiers.GetByUser(r.Context(), user.ID)
} }
return adapters.Query(fn, http.StatusOK) return adapters.Query(fn, http.StatusOK)
@ -38,10 +37,10 @@ func (ctrl *V1Controller) HandleGetUserNotifiers() server.HandlerFunc {
// @Success 200 {object} repo.NotifierOut // @Success 200 {object} repo.NotifierOut
// @Router /v1/notifiers [POST] // @Router /v1/notifiers [POST]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleCreateNotifier() server.HandlerFunc { func (ctrl *V1Controller) HandleCreateNotifier() errchain.HandlerFunc {
fn := func(ctx context.Context, in repo.NotifierCreate) (repo.NotifierOut, error) { fn := func(r *http.Request, in repo.NotifierCreate) (repo.NotifierOut, error) {
auth := services.NewContext(ctx) auth := services.NewContext(r.Context())
return ctrl.repo.Notifiers.Create(ctx, auth.GID, auth.UID, in) return ctrl.repo.Notifiers.Create(auth, auth.GID, auth.UID, in)
} }
return adapters.Action(fn, http.StatusCreated) return adapters.Action(fn, http.StatusCreated)
@ -55,10 +54,10 @@ func (ctrl *V1Controller) HandleCreateNotifier() server.HandlerFunc {
// @Success 204 // @Success 204
// @Router /v1/notifiers/{id} [DELETE] // @Router /v1/notifiers/{id} [DELETE]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleDeleteNotifier() server.HandlerFunc { func (ctrl *V1Controller) HandleDeleteNotifier() errchain.HandlerFunc {
fn := func(ctx context.Context, ID uuid.UUID) (any, error) { fn := func(r *http.Request, ID uuid.UUID) (any, error) {
auth := services.NewContext(ctx) auth := services.NewContext(r.Context())
return nil, ctrl.repo.Notifiers.Delete(ctx, auth.UID, ID) return nil, ctrl.repo.Notifiers.Delete(auth, auth.UID, ID)
} }
return adapters.CommandID("id", fn, http.StatusNoContent) return adapters.CommandID("id", fn, http.StatusNoContent)
@ -73,10 +72,10 @@ func (ctrl *V1Controller) HandleDeleteNotifier() server.HandlerFunc {
// @Success 200 {object} repo.NotifierOut // @Success 200 {object} repo.NotifierOut
// @Router /v1/notifiers/{id} [PUT] // @Router /v1/notifiers/{id} [PUT]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleUpdateNotifier() server.HandlerFunc { func (ctrl *V1Controller) HandleUpdateNotifier() errchain.HandlerFunc {
fn := func(ctx context.Context, ID uuid.UUID, in repo.NotifierUpdate) (repo.NotifierOut, error) { fn := func(r *http.Request, ID uuid.UUID, in repo.NotifierUpdate) (repo.NotifierOut, error) {
auth := services.NewContext(ctx) auth := services.NewContext(r.Context())
return ctrl.repo.Notifiers.Update(ctx, auth.UID, ID, in) return ctrl.repo.Notifiers.Update(auth, auth.UID, ID, in)
} }
return adapters.ActionID("id", fn, http.StatusOK) return adapters.ActionID("id", fn, http.StatusOK)
@ -92,12 +91,12 @@ func (ctrl *V1Controller) HandleUpdateNotifier() server.HandlerFunc {
// @Success 204 // @Success 204
// @Router /v1/notifiers/test [POST] // @Router /v1/notifiers/test [POST]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandlerNotifierTest() server.HandlerFunc { func (ctrl *V1Controller) HandlerNotifierTest() errchain.HandlerFunc {
type body struct { type body struct {
URL string `json:"url" validate:"required"` URL string `json:"url" validate:"required"`
} }
fn := func(ctx context.Context, q body) (any, error) { fn := func(r *http.Request, q body) (any, error) {
err := shoutrrr.Send(q.URL, "Test message from Homebox") err := shoutrrr.Send(q.URL, "Test message from Homebox")
return nil, err return nil, err
} }

View file

@ -6,8 +6,8 @@ import (
"io" "io"
"net/http" "net/http"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
"github.com/yeqown/go-qrcode/v2" "github.com/yeqown/go-qrcode/v2"
"github.com/yeqown/go-qrcode/writer/standard" "github.com/yeqown/go-qrcode/writer/standard"
@ -26,25 +26,24 @@ var qrcodeLogo []byte
// @Success 200 {string} string "image/jpeg" // @Success 200 {string} string "image/jpeg"
// @Router /v1/qrcode [GET] // @Router /v1/qrcode [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGenerateQRCode() server.HandlerFunc { func (ctrl *V1Controller) HandleGenerateQRCode() errchain.HandlerFunc {
const MaxLength = 4_296 // assume alphanumeric characters only type query struct {
// 4,296 characters is the maximum length of a QR code
Data string `schema:"data" validate:"required,max=4296"`
}
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
data := r.URL.Query().Get("data") q, err := adapters.DecodeQuery[query](r)
if err != nil {
return err
}
image, err := png.Decode(bytes.NewReader(qrcodeLogo)) image, err := png.Decode(bytes.NewReader(qrcodeLogo))
if err != nil { if err != nil {
panic(err) panic(err)
} }
if len(data) > MaxLength { qrc, err := qrcode.New(q.Data)
return validate.NewFieldErrors(validate.FieldError{
Field: "data",
Error: "max length is 4,296 characters exceeded",
})
}
qrc, err := qrcode.New(data)
if err != nil { if err != nil {
return err return err
} }

View file

@ -4,7 +4,7 @@ import (
"net/http" "net/http"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
) )
// HandleBillOfMaterialsExport godoc // HandleBillOfMaterialsExport godoc
@ -15,7 +15,7 @@ import (
// @Success 200 {string} string "text/csv" // @Success 200 {string} string "text/csv"
// @Router /v1/reporting/bill-of-materials [GET] // @Router /v1/reporting/bill-of-materials [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleBillOfMaterialsExport() server.HandlerFunc { func (ctrl *V1Controller) HandleBillOfMaterialsExport() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
actor := services.UseUserCtx(r.Context()) actor := services.UseUserCtx(r.Context())

View file

@ -5,8 +5,11 @@ import (
"time" "time"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
) )
// HandleGroupGet godoc // HandleGroupGet godoc
@ -17,17 +20,13 @@ import (
// @Success 200 {object} []repo.TotalsByOrganizer // @Success 200 {object} []repo.TotalsByOrganizer
// @Router /v1/groups/statistics/locations [GET] // @Router /v1/groups/statistics/locations [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGroupStatisticsLocations() server.HandlerFunc { func (ctrl *V1Controller) HandleGroupStatisticsLocations() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { fn := func(r *http.Request) ([]repo.TotalsByOrganizer, error) {
ctx := services.NewContext(r.Context()) auth := services.NewContext(r.Context())
return ctrl.repo.Groups.StatsLocationsByPurchasePrice(auth, auth.GID)
stats, err := ctrl.repo.Groups.StatsLocationsByPurchasePrice(ctx, ctx.GID)
if err != nil {
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, stats)
} }
return adapters.Command(fn, http.StatusOK)
} }
// HandleGroupStatisticsLabels godoc // HandleGroupStatisticsLabels godoc
@ -38,17 +37,13 @@ func (ctrl *V1Controller) HandleGroupStatisticsLocations() server.HandlerFunc {
// @Success 200 {object} []repo.TotalsByOrganizer // @Success 200 {object} []repo.TotalsByOrganizer
// @Router /v1/groups/statistics/labels [GET] // @Router /v1/groups/statistics/labels [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGroupStatisticsLabels() server.HandlerFunc { func (ctrl *V1Controller) HandleGroupStatisticsLabels() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { fn := func(r *http.Request) ([]repo.TotalsByOrganizer, error) {
ctx := services.NewContext(r.Context()) auth := services.NewContext(r.Context())
return ctrl.repo.Groups.StatsLabelsByPurchasePrice(auth, auth.GID)
stats, err := ctrl.repo.Groups.StatsLabelsByPurchasePrice(ctx, ctx.GID)
if err != nil {
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, stats)
} }
return adapters.Command(fn, http.StatusOK)
} }
// HandleGroupStatistics godoc // HandleGroupStatistics godoc
@ -59,17 +54,13 @@ func (ctrl *V1Controller) HandleGroupStatisticsLabels() server.HandlerFunc {
// @Success 200 {object} repo.GroupStatistics // @Success 200 {object} repo.GroupStatistics
// @Router /v1/groups/statistics [GET] // @Router /v1/groups/statistics [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGroupStatistics() server.HandlerFunc { func (ctrl *V1Controller) HandleGroupStatistics() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { fn := func(r *http.Request) (repo.GroupStatistics, error) {
ctx := services.NewContext(r.Context()) auth := services.NewContext(r.Context())
return ctrl.repo.Groups.StatsGroup(auth, auth.GID)
stats, err := ctrl.repo.Groups.StatsGroup(ctx, ctx.GID)
if err != nil {
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, stats)
} }
return adapters.Command(fn, http.StatusOK)
} }
// HandleGroupStatisticsPriceOverTime godoc // HandleGroupStatisticsPriceOverTime godoc
@ -82,7 +73,7 @@ func (ctrl *V1Controller) HandleGroupStatistics() server.HandlerFunc {
// @Param end query string false "end date" // @Param end query string false "end date"
// @Router /v1/groups/statistics/purchase-price [GET] // @Router /v1/groups/statistics/purchase-price [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGroupStatisticsPriceOverTime() server.HandlerFunc { func (ctrl *V1Controller) HandleGroupStatisticsPriceOverTime() errchain.HandlerFunc {
parseDate := func(datestr string, defaultDate time.Time) (time.Time, error) { parseDate := func(datestr string, defaultDate time.Time) (time.Time, error) {
if datestr == "" { if datestr == "" {
return defaultDate, nil return defaultDate, nil
@ -108,6 +99,6 @@ func (ctrl *V1Controller) HandleGroupStatisticsPriceOverTime() server.HandlerFun
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.Respond(w, http.StatusOK, stats) return server.JSON(w, http.StatusOK, stats)
} }
} }

View file

@ -8,7 +8,8 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -20,7 +21,7 @@ import (
// @Param payload body services.UserRegistration true "User Data" // @Param payload body services.UserRegistration true "User Data"
// @Success 204 // @Success 204
// @Router /v1/users/register [Post] // @Router /v1/users/register [Post]
func (ctrl *V1Controller) HandleUserRegistration() server.HandlerFunc { func (ctrl *V1Controller) HandleUserRegistration() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
regData := services.UserRegistration{} regData := services.UserRegistration{}
@ -39,7 +40,7 @@ func (ctrl *V1Controller) HandleUserRegistration() server.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.Respond(w, http.StatusNoContent, nil) return server.JSON(w, http.StatusNoContent, nil)
} }
} }
@ -48,10 +49,10 @@ func (ctrl *V1Controller) HandleUserRegistration() server.HandlerFunc {
// @Summary Get User Self // @Summary Get User Self
// @Tags User // @Tags User
// @Produce json // @Produce json
// @Success 200 {object} server.Result{item=repo.UserOut} // @Success 200 {object} Wrapped{item=repo.UserOut}
// @Router /v1/users/self [GET] // @Router /v1/users/self [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleUserSelf() server.HandlerFunc { func (ctrl *V1Controller) HandleUserSelf() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
token := services.UseTokenCtx(r.Context()) token := services.UseTokenCtx(r.Context())
usr, err := ctrl.svc.User.GetSelf(r.Context(), token) usr, err := ctrl.svc.User.GetSelf(r.Context(), token)
@ -60,7 +61,7 @@ func (ctrl *V1Controller) HandleUserSelf() server.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.Respond(w, http.StatusOK, server.Wrap(usr)) return server.JSON(w, http.StatusOK, Wrap(usr))
} }
} }
@ -70,10 +71,10 @@ func (ctrl *V1Controller) HandleUserSelf() server.HandlerFunc {
// @Tags User // @Tags User
// @Produce json // @Produce json
// @Param payload body repo.UserUpdate true "User Data" // @Param payload body repo.UserUpdate true "User Data"
// @Success 200 {object} server.Result{item=repo.UserUpdate} // @Success 200 {object} Wrapped{item=repo.UserUpdate}
// @Router /v1/users/self [PUT] // @Router /v1/users/self [PUT]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleUserSelfUpdate() server.HandlerFunc { func (ctrl *V1Controller) HandleUserSelfUpdate() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
updateData := repo.UserUpdate{} updateData := repo.UserUpdate{}
if err := server.Decode(r, &updateData); err != nil { if err := server.Decode(r, &updateData); err != nil {
@ -87,7 +88,7 @@ func (ctrl *V1Controller) HandleUserSelfUpdate() server.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.Respond(w, http.StatusOK, server.Wrap(newData)) return server.JSON(w, http.StatusOK, Wrap(newData))
} }
} }
@ -99,7 +100,7 @@ func (ctrl *V1Controller) HandleUserSelfUpdate() server.HandlerFunc {
// @Success 204 // @Success 204
// @Router /v1/users/self [DELETE] // @Router /v1/users/self [DELETE]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleUserSelfDelete() server.HandlerFunc { func (ctrl *V1Controller) HandleUserSelfDelete() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
if ctrl.isDemo { if ctrl.isDemo {
return validate.NewRequestError(nil, http.StatusForbidden) return validate.NewRequestError(nil, http.StatusForbidden)
@ -110,7 +111,7 @@ func (ctrl *V1Controller) HandleUserSelfDelete() server.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.Respond(w, http.StatusNoContent, nil) return server.JSON(w, http.StatusNoContent, nil)
} }
} }
@ -129,7 +130,7 @@ type (
// @Param payload body ChangePassword true "Password Payload" // @Param payload body ChangePassword true "Password Payload"
// @Router /v1/users/change-password [PUT] // @Router /v1/users/change-password [PUT]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleUserSelfChangePassword() server.HandlerFunc { func (ctrl *V1Controller) HandleUserSelfChangePassword() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
if ctrl.isDemo { if ctrl.isDemo {
return validate.NewRequestError(nil, http.StatusForbidden) return validate.NewRequestError(nil, http.StatusForbidden)
@ -148,6 +149,6 @@ func (ctrl *V1Controller) HandleUserSelfChangePassword() server.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.Respond(w, http.StatusNoContent, nil) return server.JSON(w, http.StatusNoContent, nil)
} }
} }

View file

@ -9,6 +9,9 @@ import (
atlas "ariga.io/atlas/sql/migrate" atlas "ariga.io/atlas/sql/migrate"
"entgo.io/ent/dialect/sql/schema" "entgo.io/ent/dialect/sql/schema"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/hay-kot/homebox/backend/app/api/static/docs" "github.com/hay-kot/homebox/backend/app/api/static/docs"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent"
@ -16,9 +19,12 @@ import (
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/config" "github.com/hay-kot/homebox/backend/internal/sys/config"
"github.com/hay-kot/homebox/backend/internal/web/mid" "github.com/hay-kot/homebox/backend/internal/web/mid"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/rs/zerolog/pkgerrors"
) )
var ( var (
@ -38,6 +44,8 @@ var (
// @name Authorization // @name Authorization
// @description "Type 'Bearer TOKEN' to correctly set the API Key" // @description "Type 'Bearer TOKEN' to correctly set the API Key"
func main() { func main() {
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
cfg, err := config.New() cfg, err := config.New()
if err != nil { if err != nil {
panic(err) panic(err)
@ -118,26 +126,27 @@ func run(cfg *config.Config) error {
) )
// ========================================================================= // =========================================================================
// Start Server\ // Start Server
logger := log.With().Caller().Logger() logger := log.With().Caller().Logger()
mwLogger := mid.Logger(logger) router := chi.NewMux()
if app.conf.Mode == config.ModeDevelopment { router.Use(
mwLogger = mid.SugarLogger(logger) middleware.RequestID,
} middleware.RealIP,
mid.Logger(logger),
middleware.Recoverer,
middleware.StripSlashes,
)
chain := errchain.New(mid.Errors(app.server, logger))
app.mountRoutes(router, chain, app.repos)
app.server = server.NewServer( app.server = server.NewServer(
server.WithHost(app.conf.Web.Host), server.WithHost(app.conf.Web.Host),
server.WithPort(app.conf.Web.Port), server.WithPort(app.conf.Web.Port),
server.WithMiddleware(
mwLogger,
mid.Errors(logger),
mid.Panic(app.conf.Mode == config.ModeDevelopment),
),
) )
app.mountRoutes(app.repos)
log.Info().Msgf("Starting HTTP Server on %s:%s", app.server.Host, app.server.Port) log.Info().Msgf("Starting HTTP Server on %s:%s", app.server.Host, app.server.Port)
// ========================================================================= // =========================================================================
@ -175,5 +184,5 @@ func run(cfg *config.Config) error {
}() }()
} }
return app.server.Start() return app.server.Start(router)
} }

View file

@ -9,7 +9,7 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
) )
type tokenHasKey struct { type tokenHasKey struct {
@ -30,9 +30,9 @@ const (
// the required roles, a 403 Forbidden will be returned. // the required roles, a 403 Forbidden will be returned.
// //
// WARNING: This middleware _MUST_ be called after mwAuthToken or else it will panic // WARNING: This middleware _MUST_ be called after mwAuthToken or else it will panic
func (a *app) mwRoles(rm RoleMode, required ...string) server.Middleware { func (a *app) mwRoles(rm RoleMode, required ...string) errchain.Middleware {
return func(next server.Handler) server.Handler { return func(next errchain.Handler) errchain.Handler {
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return errchain.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context() ctx := r.Context()
maybeToken := ctx.Value(hashedToken) maybeToken := ctx.Value(hashedToken)
@ -116,8 +116,8 @@ func getCookie(r *http.Request) (string, error) {
// - header = "Bearer 1234567890" // - header = "Bearer 1234567890"
// - query = "?access_token=1234567890" // - query = "?access_token=1234567890"
// - cookie = hb.auth.token = 1234567890 // - cookie = hb.auth.token = 1234567890
func (a *app) mwAuthToken(next server.Handler) server.Handler { func (a *app) mwAuthToken(next errchain.Handler) errchain.Handler {
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return errchain.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
keyFuncs := [...]KeyFunc{ keyFuncs := [...]KeyFunc{
getBearer, getBearer,
getCookie, getCookie,

View file

@ -10,12 +10,13 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"github.com/go-chi/chi/v5"
"github.com/hay-kot/homebox/backend/app/api/handlers/debughandlers" "github.com/hay-kot/homebox/backend/app/api/handlers/debughandlers"
v1 "github.com/hay-kot/homebox/backend/app/api/handlers/v1" v1 "github.com/hay-kot/homebox/backend/app/api/handlers/v1"
_ "github.com/hay-kot/homebox/backend/app/api/static/docs" _ "github.com/hay-kot/homebox/backend/app/api/static/docs"
"github.com/hay-kot/homebox/backend/internal/data/ent/authroles" "github.com/hay-kot/homebox/backend/internal/data/ent/authroles"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
httpSwagger "github.com/swaggo/http-swagger" // http-swagger middleware httpSwagger "github.com/swaggo/http-swagger" // http-swagger middleware
) )
@ -36,12 +37,12 @@ func (a *app) debugRouter() *http.ServeMux {
} }
// registerRoutes registers all the routes for the API // registerRoutes registers all the routes for the API
func (a *app) mountRoutes(repos *repo.AllRepos) { func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllRepos) {
registerMimes() registerMimes()
a.server.Get("/swagger/*", server.ToHandler(httpSwagger.Handler( r.Get("/swagger/*", httpSwagger.Handler(
httpSwagger.URL(fmt.Sprintf("%s://%s/swagger/doc.json", a.conf.Swagger.Scheme, a.conf.Swagger.Host)), httpSwagger.URL(fmt.Sprintf("%s://%s/swagger/doc.json", a.conf.Swagger.Scheme, a.conf.Swagger.Host)),
))) ))
// ========================================================================= // =========================================================================
// API Version 1 // API Version 1
@ -56,99 +57,103 @@ func (a *app) mountRoutes(repos *repo.AllRepos) {
v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode
) )
a.server.Get(v1Base("/status"), v1Ctrl.HandleBase(func() bool { return true }, v1.Build{ r.Get(v1Base("/status"), chain.ToHandlerFunc(v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
Version: version, Version: version,
Commit: commit, Commit: commit,
BuildTime: buildTime, BuildTime: buildTime,
})) })))
a.server.Post(v1Base("/users/register"), v1Ctrl.HandleUserRegistration()) r.Post(v1Base("/users/register"), chain.ToHandlerFunc(v1Ctrl.HandleUserRegistration()))
a.server.Post(v1Base("/users/login"), v1Ctrl.HandleAuthLogin()) r.Post(v1Base("/users/login"), chain.ToHandlerFunc(v1Ctrl.HandleAuthLogin()))
userMW := []server.Middleware{ userMW := []errchain.Middleware{
a.mwAuthToken, a.mwAuthToken,
a.mwRoles(RoleModeOr, authroles.RoleUser.String()), a.mwRoles(RoleModeOr, authroles.RoleUser.String()),
} }
a.server.Get(v1Base("/users/self"), v1Ctrl.HandleUserSelf(), userMW...) r.Get(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelf(), userMW...))
a.server.Put(v1Base("/users/self"), v1Ctrl.HandleUserSelfUpdate(), userMW...) r.Put(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfUpdate(), userMW...))
a.server.Delete(v1Base("/users/self"), v1Ctrl.HandleUserSelfDelete(), userMW...) r.Delete(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfDelete(), userMW...))
a.server.Post(v1Base("/users/logout"), v1Ctrl.HandleAuthLogout(), userMW...) r.Post(v1Base("/users/logout"), chain.ToHandlerFunc(v1Ctrl.HandleAuthLogout(), userMW...))
a.server.Get(v1Base("/users/refresh"), v1Ctrl.HandleAuthRefresh(), userMW...) r.Get(v1Base("/users/refresh"), chain.ToHandlerFunc(v1Ctrl.HandleAuthRefresh(), userMW...))
a.server.Put(v1Base("/users/self/change-password"), v1Ctrl.HandleUserSelfChangePassword(), userMW...) r.Put(v1Base("/users/self/change-password"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfChangePassword(), userMW...))
a.server.Post(v1Base("/groups/invitations"), v1Ctrl.HandleGroupInvitationsCreate(), userMW...) r.Post(v1Base("/groups/invitations"), chain.ToHandlerFunc(v1Ctrl.HandleGroupInvitationsCreate(), userMW...))
a.server.Get(v1Base("/groups/statistics"), v1Ctrl.HandleGroupStatistics(), userMW...) r.Get(v1Base("/groups/statistics"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatistics(), userMW...))
a.server.Get(v1Base("/groups/statistics/purchase-price"), v1Ctrl.HandleGroupStatisticsPriceOverTime(), userMW...) r.Get(v1Base("/groups/statistics/purchase-price"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsPriceOverTime(), userMW...))
a.server.Get(v1Base("/groups/statistics/locations"), v1Ctrl.HandleGroupStatisticsLocations(), userMW...) r.Get(v1Base("/groups/statistics/locations"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLocations(), userMW...))
a.server.Get(v1Base("/groups/statistics/labels"), v1Ctrl.HandleGroupStatisticsLabels(), userMW...) r.Get(v1Base("/groups/statistics/labels"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLabels(), userMW...))
// TODO: I don't like /groups being the URL for users // TODO: I don't like /groups being the URL for users
a.server.Get(v1Base("/groups"), v1Ctrl.HandleGroupGet(), userMW...) r.Get(v1Base("/groups"), chain.ToHandlerFunc(v1Ctrl.HandleGroupGet(), userMW...))
a.server.Put(v1Base("/groups"), v1Ctrl.HandleGroupUpdate(), userMW...) r.Put(v1Base("/groups"), chain.ToHandlerFunc(v1Ctrl.HandleGroupUpdate(), userMW...))
a.server.Post(v1Base("/actions/ensure-asset-ids"), v1Ctrl.HandleEnsureAssetID(), userMW...) r.Post(v1Base("/actions/ensure-asset-ids"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureAssetID(), userMW...))
a.server.Post(v1Base("/actions/zero-item-time-fields"), v1Ctrl.HandleItemDateZeroOut(), userMW...) r.Post(v1Base("/actions/zero-item-time-fields"), chain.ToHandlerFunc(v1Ctrl.HandleItemDateZeroOut(), userMW...))
a.server.Post(v1Base("/actions/ensure-import-refs"), v1Ctrl.HandleEnsureImportRefs(), userMW...) r.Post(v1Base("/actions/ensure-import-refs"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
a.server.Get(v1Base("/locations"), v1Ctrl.HandleLocationGetAll(), userMW...) r.Get(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
a.server.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate(), userMW...) r.Post(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))
a.server.Get(v1Base("/locations/tree"), v1Ctrl.HandleLocationTreeQuery(), userMW...) r.Get(v1Base("/locations/tree"), chain.ToHandlerFunc(v1Ctrl.HandleLocationTreeQuery(), userMW...))
a.server.Get(v1Base("/locations/{id}"), v1Ctrl.HandleLocationGet(), userMW...) r.Get(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationGet(), userMW...))
a.server.Put(v1Base("/locations/{id}"), v1Ctrl.HandleLocationUpdate(), userMW...) r.Put(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationUpdate(), userMW...))
a.server.Delete(v1Base("/locations/{id}"), v1Ctrl.HandleLocationDelete(), userMW...) r.Delete(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationDelete(), userMW...))
a.server.Get(v1Base("/labels"), v1Ctrl.HandleLabelsGetAll(), userMW...) r.Get(v1Base("/labels"), chain.ToHandlerFunc(v1Ctrl.HandleLabelsGetAll(), userMW...))
a.server.Post(v1Base("/labels"), v1Ctrl.HandleLabelsCreate(), userMW...) r.Post(v1Base("/labels"), chain.ToHandlerFunc(v1Ctrl.HandleLabelsCreate(), userMW...))
a.server.Get(v1Base("/labels/{id}"), v1Ctrl.HandleLabelGet(), userMW...) r.Get(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelGet(), userMW...))
a.server.Put(v1Base("/labels/{id}"), v1Ctrl.HandleLabelUpdate(), userMW...) r.Put(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelUpdate(), userMW...))
a.server.Delete(v1Base("/labels/{id}"), v1Ctrl.HandleLabelDelete(), userMW...) r.Delete(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelDelete(), userMW...))
a.server.Get(v1Base("/items"), v1Ctrl.HandleItemsGetAll(), userMW...) r.Get(v1Base("/items"), chain.ToHandlerFunc(v1Ctrl.HandleItemsGetAll(), userMW...))
a.server.Post(v1Base("/items"), v1Ctrl.HandleItemsCreate(), userMW...) r.Post(v1Base("/items"), chain.ToHandlerFunc(v1Ctrl.HandleItemsCreate(), userMW...))
a.server.Post(v1Base("/items/import"), v1Ctrl.HandleItemsImport(), userMW...) r.Post(v1Base("/items/import"), chain.ToHandlerFunc(v1Ctrl.HandleItemsImport(), userMW...))
a.server.Get(v1Base("/items/export"), v1Ctrl.HandleItemsExport(), userMW...) r.Get(v1Base("/items/export"), chain.ToHandlerFunc(v1Ctrl.HandleItemsExport(), userMW...))
a.server.Get(v1Base("/items/fields"), v1Ctrl.HandleGetAllCustomFieldNames(), userMW...) r.Get(v1Base("/items/fields"), chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldNames(), userMW...))
a.server.Get(v1Base("/items/fields/values"), v1Ctrl.HandleGetAllCustomFieldValues(), userMW...) r.Get(v1Base("/items/fields/values"), chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldValues(), userMW...))
a.server.Get(v1Base("/items/{id}"), v1Ctrl.HandleItemGet(), userMW...) r.Get(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemGet(), userMW...))
a.server.Put(v1Base("/items/{id}"), v1Ctrl.HandleItemUpdate(), userMW...) r.Put(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...))
a.server.Delete(v1Base("/items/{id}"), v1Ctrl.HandleItemDelete(), userMW...) r.Delete(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...))
a.server.Post(v1Base("/items/{id}/attachments"), v1Ctrl.HandleItemAttachmentCreate(), userMW...) r.Post(v1Base("/items/{id}/attachments"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...))
a.server.Put(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentUpdate(), userMW...) r.Put(v1Base("/items/{id}/attachments/{attachment_id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...))
a.server.Delete(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentDelete(), userMW...) r.Delete(v1Base("/items/{id}/attachments/{attachment_id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentDelete(), userMW...))
a.server.Get(v1Base("/items/{id}/maintenance"), v1Ctrl.HandleMaintenanceEntryCreate(), userMW...) r.Get(v1Base("/items/{id}/maintenance"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceLogGet(), userMW...))
a.server.Post(v1Base("/items/{id}/maintenance"), v1Ctrl.HandleMaintenanceEntryCreate(), userMW...) r.Post(v1Base("/items/{id}/maintenance"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryCreate(), userMW...))
a.server.Put(v1Base("/items/{id}/maintenance/{entry_id}"), v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...) r.Put(v1Base("/items/{id}/maintenance/{entry_id}"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...))
a.server.Delete(v1Base("/items/{id}/maintenance/{entry_id}"), v1Ctrl.HandleMaintenanceEntryDelete(), userMW...) r.Delete(v1Base("/items/{id}/maintenance/{entry_id}"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryDelete(), userMW...))
a.server.Get(v1Base("/asset/{id}"), v1Ctrl.HandleAssetGet(), userMW...) r.Get(v1Base("/asset/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleAssetGet(), userMW...))
// Notifiers // Notifiers
a.server.Get(v1Base("/notifiers"), v1Ctrl.HandleGetUserNotifiers(), userMW...) r.Get(v1Base("/notifiers"), chain.ToHandlerFunc(v1Ctrl.HandleGetUserNotifiers(), userMW...))
a.server.Post(v1Base("/notifiers"), v1Ctrl.HandleCreateNotifier(), userMW...) r.Post(v1Base("/notifiers"), chain.ToHandlerFunc(v1Ctrl.HandleCreateNotifier(), userMW...))
a.server.Put(v1Base("/notifiers/{id}"), v1Ctrl.HandleUpdateNotifier(), userMW...) r.Put(v1Base("/notifiers/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleUpdateNotifier(), userMW...))
a.server.Delete(v1Base("/notifiers/{id}"), v1Ctrl.HandleDeleteNotifier(), userMW...) r.Delete(v1Base("/notifiers/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleDeleteNotifier(), userMW...))
a.server.Post(v1Base("/notifiers/test"), v1Ctrl.HandlerNotifierTest(), userMW...) r.Post(v1Base("/notifiers/test"), chain.ToHandlerFunc(v1Ctrl.HandlerNotifierTest(), userMW...))
// Asset-Like endpoints // Asset-Like endpoints
a.server.Get( assetMW := []errchain.Middleware{
a.mwAuthToken,
a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
}
r.Get(
v1Base("/qrcode"), v1Base("/qrcode"),
v1Ctrl.HandleGenerateQRCode(), chain.ToHandlerFunc(v1Ctrl.HandleGenerateQRCode(), assetMW...),
a.mwAuthToken, a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
) )
a.server.Get( r.Get(
v1Base("/items/{id}/attachments/{attachment_id}"), v1Base("/items/{id}/attachments/{attachment_id}"),
v1Ctrl.HandleItemAttachmentGet(), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentGet(), assetMW...),
a.mwAuthToken, a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
) )
// Reporting Services // Reporting Services
a.server.Get(v1Base("/reporting/bill-of-materials"), v1Ctrl.HandleBillOfMaterialsExport(), userMW...) r.Get(v1Base("/reporting/bill-of-materials"), chain.ToHandlerFunc(v1Ctrl.HandleBillOfMaterialsExport(), userMW...))
r.NotFound(chain.ToHandlerFunc(notFoundHandler()))
a.server.NotFound(notFoundHandler())
} }
func registerMimes() { func registerMimes() {
@ -165,7 +170,7 @@ func registerMimes() {
// notFoundHandler perform the main logic around handling the internal SPA embed and ensuring that // notFoundHandler perform the main logic around handling the internal SPA embed and ensuring that
// the client side routing is handled correctly. // the client side routing is handled correctly.
func notFoundHandler() server.HandlerFunc { func notFoundHandler() errchain.HandlerFunc {
tryRead := func(fs embed.FS, prefix, requestedPath string, w http.ResponseWriter) error { tryRead := func(fs embed.FS, prefix, requestedPath string, w http.ResponseWriter) error {
f, err := fs.Open(path.Join(prefix, requestedPath)) f, err := fs.Open(path.Join(prefix, requestedPath))
if err != nil { if err != nil {

View file

@ -425,8 +425,8 @@ const docTemplate = `{
} }
], ],
"responses": { "responses": {
"200": { "201": {
"description": "OK", "description": "Created",
"schema": { "schema": {
"$ref": "#/definitions/repo.ItemSummary" "$ref": "#/definitions/repo.ItemSummary"
} }
@ -694,7 +694,7 @@ const docTemplate = `{
"422": { "422": {
"description": "Unprocessable Entity", "description": "Unprocessable Entity",
"schema": { "schema": {
"$ref": "#/definitions/server.ErrorResponse" "$ref": "#/definitions/mid.ErrorResponse"
} }
} }
} }
@ -864,8 +864,8 @@ const docTemplate = `{
} }
], ],
"responses": { "responses": {
"200": { "201": {
"description": "OK", "description": "Created",
"schema": { "schema": {
"$ref": "#/definitions/repo.MaintenanceEntry" "$ref": "#/definitions/repo.MaintenanceEntry"
} }
@ -947,7 +947,7 @@ const docTemplate = `{
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Results" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1119,7 +1119,7 @@ const docTemplate = `{
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Results" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1199,7 +1199,7 @@ const docTemplate = `{
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Results" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1339,7 +1339,7 @@ const docTemplate = `{
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Results" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1719,7 +1719,7 @@ const docTemplate = `{
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Result" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1764,7 +1764,7 @@ const docTemplate = `{
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Result" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1801,6 +1801,20 @@ const docTemplate = `{
} }
}, },
"definitions": { "definitions": {
"mid.ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"fields": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"repo.DocumentOut": { "repo.DocumentOut": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1902,9 +1916,15 @@ const docTemplate = `{
}, },
"repo.ItemCreate": { "repo.ItemCreate": {
"type": "object", "type": "object",
"required": [
"description",
"name"
],
"properties": { "properties": {
"description": { "description": {
"type": "string" "type": "string",
"maxLength": 1000,
"minLength": 1
}, },
"labelIds": { "labelIds": {
"type": "array", "type": "array",
@ -1917,7 +1937,9 @@ const docTemplate = `{
"type": "string" "type": "string"
}, },
"name": { "name": {
"type": "string" "type": "string",
"maxLength": 255,
"minLength": 1
}, },
"parentId": { "parentId": {
"type": "string", "type": "string",
@ -2208,15 +2230,21 @@ const docTemplate = `{
}, },
"repo.LabelCreate": { "repo.LabelCreate": {
"type": "object", "type": "object",
"required": [
"name"
],
"properties": { "properties": {
"color": { "color": {
"type": "string" "type": "string"
}, },
"description": { "description": {
"type": "string" "type": "string",
"maxLength": 255
}, },
"name": { "name": {
"type": "string" "type": "string",
"maxLength": 255,
"minLength": 1
} }
} }
}, },
@ -2663,39 +2691,6 @@ const docTemplate = `{
} }
} }
}, },
"server.ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"fields": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"server.Result": {
"type": "object",
"properties": {
"details": {},
"error": {
"type": "boolean"
},
"item": {},
"message": {
"type": "string"
}
}
},
"server.Results": {
"type": "object",
"properties": {
"items": {}
}
},
"services.UserRegistration": { "services.UserRegistration": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -2791,12 +2786,17 @@ const docTemplate = `{
}, },
"v1.GroupInvitationCreate": { "v1.GroupInvitationCreate": {
"type": "object", "type": "object",
"required": [
"uses"
],
"properties": { "properties": {
"expiresAt": { "expiresAt": {
"type": "string" "type": "string"
}, },
"uses": { "uses": {
"type": "integer" "type": "integer",
"maximum": 100,
"minimum": 1
} }
} }
}, },
@ -2821,6 +2821,12 @@ const docTemplate = `{
"type": "string" "type": "string"
} }
} }
},
"v1.Wrapped": {
"type": "object",
"properties": {
"item": {}
}
} }
}, },
"securityDefinitions": { "securityDefinitions": {

View file

@ -417,8 +417,8 @@
} }
], ],
"responses": { "responses": {
"200": { "201": {
"description": "OK", "description": "Created",
"schema": { "schema": {
"$ref": "#/definitions/repo.ItemSummary" "$ref": "#/definitions/repo.ItemSummary"
} }
@ -686,7 +686,7 @@
"422": { "422": {
"description": "Unprocessable Entity", "description": "Unprocessable Entity",
"schema": { "schema": {
"$ref": "#/definitions/server.ErrorResponse" "$ref": "#/definitions/mid.ErrorResponse"
} }
} }
} }
@ -856,8 +856,8 @@
} }
], ],
"responses": { "responses": {
"200": { "201": {
"description": "OK", "description": "Created",
"schema": { "schema": {
"$ref": "#/definitions/repo.MaintenanceEntry" "$ref": "#/definitions/repo.MaintenanceEntry"
} }
@ -939,7 +939,7 @@
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Results" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1111,7 +1111,7 @@
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Results" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1191,7 +1191,7 @@
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Results" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1331,7 +1331,7 @@
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Results" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1711,7 +1711,7 @@
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Result" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1756,7 +1756,7 @@
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Result" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1793,6 +1793,20 @@
} }
}, },
"definitions": { "definitions": {
"mid.ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"fields": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"repo.DocumentOut": { "repo.DocumentOut": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1894,9 +1908,15 @@
}, },
"repo.ItemCreate": { "repo.ItemCreate": {
"type": "object", "type": "object",
"required": [
"description",
"name"
],
"properties": { "properties": {
"description": { "description": {
"type": "string" "type": "string",
"maxLength": 1000,
"minLength": 1
}, },
"labelIds": { "labelIds": {
"type": "array", "type": "array",
@ -1909,7 +1929,9 @@
"type": "string" "type": "string"
}, },
"name": { "name": {
"type": "string" "type": "string",
"maxLength": 255,
"minLength": 1
}, },
"parentId": { "parentId": {
"type": "string", "type": "string",
@ -2200,15 +2222,21 @@
}, },
"repo.LabelCreate": { "repo.LabelCreate": {
"type": "object", "type": "object",
"required": [
"name"
],
"properties": { "properties": {
"color": { "color": {
"type": "string" "type": "string"
}, },
"description": { "description": {
"type": "string" "type": "string",
"maxLength": 255
}, },
"name": { "name": {
"type": "string" "type": "string",
"maxLength": 255,
"minLength": 1
} }
} }
}, },
@ -2655,39 +2683,6 @@
} }
} }
}, },
"server.ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"fields": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"server.Result": {
"type": "object",
"properties": {
"details": {},
"error": {
"type": "boolean"
},
"item": {},
"message": {
"type": "string"
}
}
},
"server.Results": {
"type": "object",
"properties": {
"items": {}
}
},
"services.UserRegistration": { "services.UserRegistration": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -2783,12 +2778,17 @@
}, },
"v1.GroupInvitationCreate": { "v1.GroupInvitationCreate": {
"type": "object", "type": "object",
"required": [
"uses"
],
"properties": { "properties": {
"expiresAt": { "expiresAt": {
"type": "string" "type": "string"
}, },
"uses": { "uses": {
"type": "integer" "type": "integer",
"maximum": 100,
"minimum": 1
} }
} }
}, },
@ -2813,6 +2813,12 @@
"type": "string" "type": "string"
} }
} }
},
"v1.Wrapped": {
"type": "object",
"properties": {
"item": {}
}
} }
}, },
"securityDefinitions": { "securityDefinitions": {

View file

@ -1,5 +1,14 @@
basePath: /api basePath: /api
definitions: definitions:
mid.ErrorResponse:
properties:
error:
type: string
fields:
additionalProperties:
type: string
type: object
type: object
repo.DocumentOut: repo.DocumentOut:
properties: properties:
id: id:
@ -67,6 +76,8 @@ definitions:
repo.ItemCreate: repo.ItemCreate:
properties: properties:
description: description:
maxLength: 1000
minLength: 1
type: string type: string
labelIds: labelIds:
items: items:
@ -76,10 +87,15 @@ definitions:
description: Edges description: Edges
type: string type: string
name: name:
maxLength: 255
minLength: 1
type: string type: string
parentId: parentId:
type: string type: string
x-nullable: true x-nullable: true
required:
- description
- name
type: object type: object
repo.ItemField: repo.ItemField:
properties: properties:
@ -281,9 +297,14 @@ definitions:
color: color:
type: string type: string
description: description:
maxLength: 255
type: string type: string
name: name:
maxLength: 255
minLength: 1
type: string type: string
required:
- name
type: object type: object
repo.LabelOut: repo.LabelOut:
properties: properties:
@ -579,28 +600,6 @@ definitions:
value: value:
type: number type: number
type: object type: object
server.ErrorResponse:
properties:
error:
type: string
fields:
additionalProperties:
type: string
type: object
type: object
server.Result:
properties:
details: {}
error:
type: boolean
item: {}
message:
type: string
type: object
server.Results:
properties:
items: {}
type: object
services.UserRegistration: services.UserRegistration:
properties: properties:
email: email:
@ -666,7 +665,11 @@ definitions:
expiresAt: expiresAt:
type: string type: string
uses: uses:
maximum: 100
minimum: 1
type: integer type: integer
required:
- uses
type: object type: object
v1.ItemAttachmentToken: v1.ItemAttachmentToken:
properties: properties:
@ -682,6 +685,10 @@ definitions:
token: token:
type: string type: string
type: object type: object
v1.Wrapped:
properties:
item: {}
type: object
info: info:
contact: contact:
name: Don't name: Don't
@ -932,8 +939,8 @@ paths:
produces: produces:
- application/json - application/json
responses: responses:
"200": "201":
description: OK description: Created
schema: schema:
$ref: '#/definitions/repo.ItemSummary' $ref: '#/definitions/repo.ItemSummary'
security: security:
@ -1036,7 +1043,7 @@ paths:
"422": "422":
description: Unprocessable Entity description: Unprocessable Entity
schema: schema:
$ref: '#/definitions/server.ErrorResponse' $ref: '#/definitions/mid.ErrorResponse'
security: security:
- Bearer: [] - Bearer: []
summary: Create Item Attachment summary: Create Item Attachment
@ -1140,8 +1147,8 @@ paths:
produces: produces:
- application/json - application/json
responses: responses:
"200": "201":
description: OK description: Created
schema: schema:
$ref: '#/definitions/repo.MaintenanceEntry' $ref: '#/definitions/repo.MaintenanceEntry'
security: security:
@ -1252,7 +1259,7 @@ paths:
description: OK description: OK
schema: schema:
allOf: allOf:
- $ref: '#/definitions/server.Results' - $ref: '#/definitions/v1.Wrapped'
- properties: - properties:
items: items:
items: items:
@ -1354,7 +1361,7 @@ paths:
description: OK description: OK
schema: schema:
allOf: allOf:
- $ref: '#/definitions/server.Results' - $ref: '#/definitions/v1.Wrapped'
- properties: - properties:
items: items:
items: items:
@ -1462,7 +1469,7 @@ paths:
description: OK description: OK
schema: schema:
allOf: allOf:
- $ref: '#/definitions/server.Results' - $ref: '#/definitions/v1.Wrapped'
- properties: - properties:
items: items:
items: items:
@ -1483,7 +1490,7 @@ paths:
description: OK description: OK
schema: schema:
allOf: allOf:
- $ref: '#/definitions/server.Results' - $ref: '#/definitions/v1.Wrapped'
- properties: - properties:
items: items:
items: items:
@ -1725,7 +1732,7 @@ paths:
description: OK description: OK
schema: schema:
allOf: allOf:
- $ref: '#/definitions/server.Result' - $ref: '#/definitions/v1.Wrapped'
- properties: - properties:
item: item:
$ref: '#/definitions/repo.UserOut' $ref: '#/definitions/repo.UserOut'
@ -1750,7 +1757,7 @@ paths:
description: OK description: OK
schema: schema:
allOf: allOf:
- $ref: '#/definitions/server.Result' - $ref: '#/definitions/v1.Wrapped'
- properties: - properties:
item: item:
$ref: '#/definitions/repo.UserUpdate' $ref: '#/definitions/repo.UserUpdate'

View file

@ -12,7 +12,9 @@ require (
github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/gorilla/schema v1.2.0 github.com/gorilla/schema v1.2.0
github.com/hay-kot/safeserve v0.0.1
github.com/mattn/go-sqlite3 v1.14.16 github.com/mattn/go-sqlite3 v1.14.16
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.29.0 github.com/rs/zerolog v1.29.0
github.com/stretchr/testify v1.8.2 github.com/stretchr/testify v1.8.2
github.com/swaggo/http-swagger v1.3.4 github.com/swaggo/http-swagger v1.3.4
@ -45,7 +47,6 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/swaggo/files v1.0.0 // indirect github.com/swaggo/files v1.0.0 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect github.com/yeqown/reedsolomon v1.0.0 // indirect

View file

@ -1,5 +1,3 @@
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb h1:mbsFtavDqGdYwdDpP50LGOOZ2hgyGoJcZeOpbgKMyu4=
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb/go.mod h1:T230JFcENj4ZZzMkZrXFDSkv+2kXkUgpJ5FQQ5hMcKU=
ariga.io/atlas v0.10.0 h1:B1aCP6gSDQET6j8ybn7m6MArjQuoLH5d4DQBT+NP5k8= ariga.io/atlas v0.10.0 h1:B1aCP6gSDQET6j8ybn7m6MArjQuoLH5d4DQBT+NP5k8=
ariga.io/atlas v0.10.0/go.mod h1:+TR129FJZ5Lvzms6dvCeGWh1yR6hMvmXBhug4hrNIGk= ariga.io/atlas v0.10.0/go.mod h1:+TR129FJZ5Lvzms6dvCeGWh1yR6hMvmXBhug4hrNIGk=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
@ -204,8 +202,6 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/ardanlabs/conf/v3 v3.1.4 h1:c0jJYbqHJcrR/uYImbGC1q7quH3DYxH49zGCT7WLJH4=
github.com/ardanlabs/conf/v3 v3.1.4/go.mod h1:bIacyuGeZjkTdtszdbvOcuq49VhHpV3+IPZ2ewOAK4I=
github.com/ardanlabs/conf/v3 v3.1.5 h1:G6df2AxKnGHAK+ur2p50Ys8Vo1HnKcsvqSj9lxVeczk= github.com/ardanlabs/conf/v3 v3.1.5 h1:G6df2AxKnGHAK+ur2p50Ys8Vo1HnKcsvqSj9lxVeczk=
github.com/ardanlabs/conf/v3 v3.1.5/go.mod h1:zclexWKe0NVj6LHQ8NgDDZ7bQ1spE0KeKPFficdtAjU= github.com/ardanlabs/conf/v3 v3.1.5/go.mod h1:zclexWKe0NVj6LHQ8NgDDZ7bQ1spE0KeKPFficdtAjU=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
@ -300,15 +296,11 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI= github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA= github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/gocarina/gocsv v0.0.0-20230219202803-bcce7dc8d0bb h1:WZ3ADdZNC1i7uJsarVzPSSh0B27+XlmmCerFmU28T/4=
github.com/gocarina/gocsv v0.0.0-20230219202803-bcce7dc8d0bb/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a h1:/5o1ejt5M0fNAN2lU1NBLtPzUSZru689EWJq01ptr+E= github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a h1:/5o1ejt5M0fNAN2lU1NBLtPzUSZru689EWJq01ptr+E=
github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@ -447,6 +439,8 @@ github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn
github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hashicorp/serf v0.9.8/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/serf v0.9.8/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hay-kot/safeserve v0.0.1 h1:9u8Ooyk8NNkqgxrqkLMWtMqauWEl/VZVtEUTLbHuAU8=
github.com/hay-kot/safeserve v0.0.1/go.mod h1:RUvwyfQTmbNgm5sHt+tQOqtdcpWadXWMhLty74Vedzw=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@ -481,8 +475,6 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4= github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4=
github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ= github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
@ -507,7 +499,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@ -533,7 +524,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
@ -585,8 +575,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
@ -603,10 +593,8 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -628,8 +616,6 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4= github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4=
github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc= github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc=
github.com/swaggo/http-swagger v1.3.3 h1:Hu5Z0L9ssyBLofaama21iYaF2VbWyA8jdohaaCGpHsc=
github.com/swaggo/http-swagger v1.3.3/go.mod h1:sE+4PjD89IxMPm77FnkDz0sdO+p5lbXzrVWT6OTVVGo=
github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww=
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ= github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo= github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo=

View file

@ -51,8 +51,8 @@ type (
ItemCreate struct { ItemCreate struct {
ImportRef string `json:"-"` ImportRef string `json:"-"`
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable"` ParentID uuid.UUID `json:"parentId" extensions:"x-nullable"`
Name string `json:"name"` Name string `json:"name" validate:"required,min=1,max=255"`
Description string `json:"description"` Description string `json:"description" validate:"required,min=1,max=1000"`
AssetID AssetID `json:"-"` AssetID AssetID `json:"-"`
// Edges // Edges

View file

@ -17,15 +17,15 @@ type LabelRepository struct {
} }
type ( type (
LabelCreate struct { LabelCreate struct {
Name string `json:"name"` Name string `json:"name" validate:"required,min=1,max=255"`
Description string `json:"description"` Description string `json:"description" validate:"max=255"`
Color string `json:"color"` Color string `json:"color"`
} }
LabelUpdate struct { LabelUpdate struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Name string `json:"name"` Name string `json:"name" validate:"required,min=1,max=255"`
Description string `json:"description"` Description string `json:"description" validate:"max=255"`
Color string `json:"color"` Color string `json:"color"`
} }

View file

@ -91,7 +91,7 @@ func mapLocationOut(location *ent.Location) LocationOut {
} }
type LocationQuery struct { type LocationQuery struct {
FilterChildren bool `json:"filterChildren"` FilterChildren bool `json:"filterChildren" schema:"filterChildren"`
} }
// GetALlWithCount returns all locations with item count field populated // GetALlWithCount returns all locations with item count field populated
@ -217,7 +217,7 @@ func (r *LocationRepository) Update(ctx context.Context, data LocationUpdate) (L
return r.update(ctx, data, location.ID(data.ID)) return r.update(ctx, data, location.ID(data.ID))
} }
func (r *LocationRepository) UpdateOneByGroup(ctx context.Context, GID, ID uuid.UUID, data LocationUpdate) (LocationOut, error) { func (r *LocationRepository) UpdateByGroup(ctx context.Context, GID, ID uuid.UUID, data LocationUpdate) (LocationOut, error) {
return r.update(ctx, data, location.ID(ID), location.HasGroupWith(group.ID(GID))) return r.update(ctx, data, location.ID(ID), location.HasGroupWith(group.ID(GID)))
} }
@ -246,7 +246,7 @@ type FlatTreeItem struct {
} }
type TreeQuery struct { type TreeQuery struct {
WithItems bool `json:"withItems"` WithItems bool `json:"withItems" schema:"withItems"`
} }
func (lr *LocationRepository) Tree(ctx context.Context, GID uuid.UUID, tq TreeQuery) ([]TreeItem, error) { func (lr *LocationRepository) Tree(ctx context.Context, GID uuid.UUID, tq TreeQuery) ([]TreeItem, error) {

View file

@ -124,7 +124,7 @@ func TestItemRepository_TreeQuery(t *testing.T) {
locs := useLocations(t, 3) locs := useLocations(t, 3)
// Set relations // Set relations
_, err := tRepos.Locations.UpdateOneByGroup(context.Background(), tGroup.ID, locs[0].ID, LocationUpdate{ _, err := tRepos.Locations.UpdateByGroup(context.Background(), tGroup.ID, locs[0].ID, LocationUpdate{
ID: locs[0].ID, ID: locs[0].ID,
ParentID: locs[1].ID, ParentID: locs[1].ID,
Name: locs[0].Name, Name: locs[0].Name,

View file

@ -6,6 +6,8 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/ent/group"
"github.com/hay-kot/homebox/backend/internal/data/ent/item"
"github.com/hay-kot/homebox/backend/internal/data/ent/maintenanceentry" "github.com/hay-kot/homebox/backend/internal/data/ent/maintenanceentry"
"github.com/hay-kot/homebox/backend/internal/data/types" "github.com/hay-kot/homebox/backend/internal/data/types"
) )
@ -92,16 +94,21 @@ func (r *MaintenanceEntryRepository) Update(ctx context.Context, ID uuid.UUID, i
} }
type MaintenanceLogQuery struct { type MaintenanceLogQuery struct {
Completed bool Completed bool `json:"completed" schema:"completed"`
Scheduled bool Scheduled bool `json:"scheduled" schema:"scheduled"`
} }
func (r *MaintenanceEntryRepository) GetLog(ctx context.Context, itemID uuid.UUID, query MaintenanceLogQuery) (MaintenanceLog, error) { func (r *MaintenanceEntryRepository) GetLog(ctx context.Context, groupID, itemID uuid.UUID, query MaintenanceLogQuery) (MaintenanceLog, error) {
log := MaintenanceLog{ log := MaintenanceLog{
ItemID: itemID, ItemID: itemID,
} }
q := r.db.MaintenanceEntry.Query().Where(maintenanceentry.ItemID(itemID)) q := r.db.MaintenanceEntry.Query().Where(
maintenanceentry.ItemID(itemID),
maintenanceentry.HasItemWith(
item.HasGroupWith(group.IDEQ(groupID)),
),
)
if query.Completed { if query.Completed {
q = q.Where(maintenanceentry.And( q = q.Where(maintenanceentry.And(

View file

@ -59,7 +59,7 @@ func TestMaintenanceEntryRepository_GetLog(t *testing.T) {
} }
// Get the log for the item // Get the log for the item
log, err := tRepos.MaintEntry.GetLog(context.Background(), item.ID, MaintenanceLogQuery{ log, err := tRepos.MaintEntry.GetLog(context.Background(), tGroup.ID, item.ID, MaintenanceLogQuery{
Completed: true, Completed: true,
}) })
if err != nil { if err != nil {

View file

@ -3,7 +3,8 @@ package adapters
import ( import (
"net/http" "net/http"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
) )
// Action is a function that adapts a function to the server.Handler interface. // Action is a function that adapts a function to the server.Handler interface.
@ -16,25 +17,25 @@ import (
// Foo string `json:"foo"` // Foo string `json:"foo"`
// } // }
// //
// fn := func(ctx context.Context, b Body) (any, error) { // fn := func(r *http.Request, b Body) (any, error) {
// // do something with b // // do something with b
// return nil, nil // return nil, nil
// } // }
// //
// r.Post("/foo", adapters.Action(fn, http.StatusCreated)) // r.Post("/foo", adapters.Action(fn, http.StatusCreated))
func Action[T any, Y any](f AdapterFunc[T, Y], ok int) server.HandlerFunc { func Action[T any, Y any](f AdapterFunc[T, Y], ok int) errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
v, err := decode[T](r) v, err := DecodeBody[T](r)
if err != nil { if err != nil {
return err return err
} }
res, err := f(r.Context(), v) res, err := f(r, v)
if err != nil { if err != nil {
return err return err
} }
return server.Respond(w, ok, res) return server.JSON(w, ok, res)
} }
} }
@ -46,29 +47,29 @@ func Action[T any, Y any](f AdapterFunc[T, Y], ok int) server.HandlerFunc {
// Foo string `json:"foo"` // Foo string `json:"foo"`
// } // }
// //
// fn := func(ctx context.Context, ID uuid.UUID, b Body) (any, error) { // fn := func(r *http.Request, ID uuid.UUID, b Body) (any, error) {
// // do something with ID and b // // do something with ID and b
// return nil, nil // return nil, nil
// } // }
// //
// r.Post("/foo/{id}", adapters.ActionID(fn, http.StatusCreated)) // r.Post("/foo/{id}", adapters.ActionID(fn, http.StatusCreated))
func ActionID[T any, Y any](param string, f IDFunc[T, Y], ok int) server.HandlerFunc { func ActionID[T any, Y any](param string, f IDFunc[T, Y], ok int) errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
ID, err := routeUUID(r, param) ID, err := RouteUUID(r, param)
if err != nil { if err != nil {
return err return err
} }
v, err := decode[T](r) v, err := DecodeBody[T](r)
if err != nil { if err != nil {
return err return err
} }
res, err := f(r.Context(), ID, v) res, err := f(r, ID, v)
if err != nil { if err != nil {
return err return err
} }
return server.Respond(w, ok, res) return server.JSON(w, ok, res)
} }
} }

View file

@ -1,10 +1,10 @@
package adapters package adapters
import ( import (
"context" "net/http"
"github.com/google/uuid" "github.com/google/uuid"
) )
type AdapterFunc[T any, Y any] func(context.Context, T) (Y, error) type AdapterFunc[T any, Y any] func(*http.Request, T) (Y, error)
type IDFunc[T any, Y any] func(context.Context, uuid.UUID, T) (Y, error) type IDFunc[T any, Y any] func(*http.Request, uuid.UUID, T) (Y, error)

View file

@ -1,36 +1,36 @@
package adapters package adapters
import ( import (
"context"
"net/http" "net/http"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
) )
type CommandFunc[T any] func(context.Context) (T, error) type CommandFunc[T any] func(*http.Request) (T, error)
type CommandIDFunc[T any] func(context.Context, uuid.UUID) (T, error) type CommandIDFunc[T any] func(*http.Request, uuid.UUID) (T, error)
// Command is an HandlerAdapter that returns a server.HandlerFunc that // Command is an HandlerAdapter that returns a errchain.HandlerFunc that
// The command adapters are used to handle commands that do not accept a body // The command adapters are used to handle commands that do not accept a body
// or a query. You can think of them as a way to handle RPC style Rest Endpoints. // or a query. You can think of them as a way to handle RPC style Rest Endpoints.
// //
// Example: // Example:
// //
// fn := func(ctx context.Context) (interface{}, error) { // fn := func(r *http.Request) (interface{}, error) {
// // do something // // do something
// return nil, nil // return nil, nil
// } // }
// //
// r.Get("/foo", adapters.Command(fn, http.NoContent)) // r.Get("/foo", adapters.Command(fn, http.NoContent))
func Command[T any](f CommandFunc[T], ok int) server.HandlerFunc { func Command[T any](f CommandFunc[T], ok int) errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
res, err := f(r.Context()) res, err := f(r)
if err != nil { if err != nil {
return err return err
} }
return server.Respond(w, ok, res) return server.JSON(w, ok, res)
} }
} }
@ -39,24 +39,24 @@ func Command[T any](f CommandFunc[T], ok int) server.HandlerFunc {
// //
// Example: // Example:
// //
// fn := func(ctx context.Context, id uuid.UUID) (interface{}, error) { // fn := func(r *http.Request, id uuid.UUID) (interface{}, error) {
// // do something // // do something
// return nil, nil // return nil, nil
// } // }
// //
// r.Get("/foo/{id}", adapters.CommandID("id", fn, http.NoContent)) // r.Get("/foo/{id}", adapters.CommandID("id", fn, http.NoContent))
func CommandID[T any](param string, f CommandIDFunc[T], ok int) server.HandlerFunc { func CommandID[T any](param string, f CommandIDFunc[T], ok int) errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
ID, err := routeUUID(r, param) ID, err := RouteUUID(r, param)
if err != nil { if err != nil {
return err return err
} }
res, err := f(r.Context(), ID) res, err := f(r, ID)
if err != nil { if err != nil {
return err return err
} }
return server.Respond(w, ok, res) return server.JSON(w, ok, res)
} }
} }

View file

@ -3,47 +3,49 @@ package adapters
import ( import (
"net/http" "net/http"
"github.com/pkg/errors"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/server"
) )
var queryDecoder = schema.NewDecoder() var queryDecoder = schema.NewDecoder()
func decodeQuery[T any](r *http.Request) (T, error) { func DecodeQuery[T any](r *http.Request) (T, error) {
var v T var v T
err := queryDecoder.Decode(&v, r.URL.Query()) err := queryDecoder.Decode(&v, r.URL.Query())
if err != nil { if err != nil {
return v, err return v, errors.Wrap(err, "decoding error")
} }
err = validate.Check(v) err = validate.Check(v)
if err != nil { if err != nil {
return v, err return v, errors.Wrap(err, "validation error")
} }
return v, nil return v, nil
} }
func decode[T any](r *http.Request) (T, error) { func DecodeBody[T any](r *http.Request) (T, error) {
var v T var v T
err := server.Decode(r, &v) err := server.Decode(r, &v)
if err != nil { if err != nil {
return v, err return v, errors.Wrap(err, "body decoding error")
} }
err = validate.Check(v) err = validate.Check(v)
if err != nil { if err != nil {
return v, err return v, errors.Wrap(err, "validation error")
} }
return v, nil return v, nil
} }
func routeUUID(r *http.Request, key string) (uuid.UUID, error) { func RouteUUID(r *http.Request, key string) (uuid.UUID, error) {
ID, err := uuid.Parse(chi.URLParam(r, key)) ID, err := uuid.Parse(chi.URLParam(r, key))
if err != nil { if err != nil {
return uuid.Nil, validate.NewRouteKeyError(key) return uuid.Nil, validate.NewRouteKeyError(key)

View file

@ -3,7 +3,8 @@ package adapters
import ( import (
"net/http" "net/http"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
) )
// Query is a server.Handler that decodes a query from the request and calls the provided function. // Query is a server.Handler that decodes a query from the request and calls the provided function.
@ -14,25 +15,25 @@ import (
// Foo string `schema:"foo"` // Foo string `schema:"foo"`
// } // }
// //
// fn := func(ctx context.Context, q Query) (any, error) { // fn := func(r *http.Request, q Query) (any, error) {
// // do something with q // // do something with q
// return nil, nil // return nil, nil
// } // }
// //
// r.Get("/foo", adapters.Query(fn, http.StatusOK)) // r.Get("/foo", adapters.Query(fn, http.StatusOK))
func Query[T any, Y any](f AdapterFunc[T, Y], ok int) server.HandlerFunc { func Query[T any, Y any](f AdapterFunc[T, Y], ok int) errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
q, err := decodeQuery[T](r) q, err := DecodeQuery[T](r)
if err != nil { if err != nil {
return err return err
} }
res, err := f(r.Context(), q) res, err := f(r, q)
if err != nil { if err != nil {
return err return err
} }
return server.Respond(w, ok, res) return server.JSON(w, ok, res)
} }
} }
@ -44,29 +45,29 @@ func Query[T any, Y any](f AdapterFunc[T, Y], ok int) server.HandlerFunc {
// Foo string `schema:"foo"` // Foo string `schema:"foo"`
// } // }
// //
// fn := func(ctx context.Context, ID uuid.UUID, q Query) (any, error) { // fn := func(r *http.Request, ID uuid.UUID, q Query) (any, error) {
// // do something with ID and q // // do something with ID and q
// return nil, nil // return nil, nil
// } // }
// //
// r.Get("/foo/{id}", adapters.QueryID(fn, http.StatusOK)) // r.Get("/foo/{id}", adapters.QueryID(fn, http.StatusOK))
func QueryID[T any, Y any](param string, f IDFunc[T, Y], ok int) server.HandlerFunc { func QueryID[T any, Y any](param string, f IDFunc[T, Y], ok int) errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
ID, err := routeUUID(r, param) ID, err := RouteUUID(r, param)
if err != nil { if err != nil {
return err return err
} }
q, err := decodeQuery[T](r) q, err := DecodeQuery[T](r)
if err != nil { if err != nil {
return err return err
} }
res, err := f(r.Context(), ID, q) res, err := f(r, ID, q)
if err != nil { if err != nil {
return err return err
} }
return server.Respond(w, ok, res) return server.JSON(w, ok, res)
} }
} }

View file

@ -3,33 +3,42 @@ package mid
import ( import (
"net/http" "net/http"
"github.com/go-chi/chi/v5/middleware"
"github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/hay-kot/safeserve/errchain"
"github.com/hay-kot/safeserve/server"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
func Errors(log zerolog.Logger) server.Middleware { type ErrorResponse struct {
return func(h server.Handler) server.Handler { Error string `json:"error"`
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { Fields map[string]string `json:"fields,omitempty"`
}
func Errors(svr *server.Server, log zerolog.Logger) errchain.ErrorHandler {
return func(h errchain.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := h.ServeHTTP(w, r) err := h.ServeHTTP(w, r)
if err != nil { if err != nil {
var resp server.ErrorResponse var resp ErrorResponse
var code int var code int
traceID := r.Context().Value(middleware.RequestIDKey).(string)
log.Err(err). log.Err(err).
Str("trace_id", server.GetTraceID(r.Context())). Stack().
Str("req_id", traceID).
Msg("ERROR occurred") Msg("ERROR occurred")
switch { switch {
case validate.IsUnauthorizedError(err): case validate.IsUnauthorizedError(err):
code = http.StatusUnauthorized code = http.StatusUnauthorized
resp = server.ErrorResponse{ resp = ErrorResponse{
Error: "unauthorized", Error: "unauthorized",
} }
case validate.IsInvalidRouteKeyError(err): case validate.IsInvalidRouteKeyError(err):
code = http.StatusBadRequest code = http.StatusBadRequest
resp = server.ErrorResponse{ resp = ErrorResponse{
Error: err.Error(), Error: err.Error(),
} }
case validate.IsFieldError(err): case validate.IsFieldError(err):
@ -59,17 +68,18 @@ func Errors(log zerolog.Logger) server.Middleware {
code = http.StatusInternalServerError code = http.StatusInternalServerError
} }
if err := server.Respond(w, code, resp); err != nil { if err := server.JSON(w, code, resp); err != nil {
return err log.Err(err).Msg("failed to write response")
} }
// If Showdown error, return error // If Showdown error, return error
if server.IsShutdownError(err) { if server.IsShutdownError(err) {
return err err := svr.Shutdown(err.Error())
if err != nil {
log.Err(err).Msg("failed to shutdown server")
}
} }
} }
return nil
}) })
} }
} }

View file

@ -1,96 +1,33 @@
package mid package mid
import ( import (
"fmt"
"net/http" "net/http"
"github.com/hay-kot/homebox/backend/pkgs/server" "github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
type statusRecorder struct { type spy struct {
http.ResponseWriter http.ResponseWriter
Status int status int
} }
func (r *statusRecorder) WriteHeader(status int) { func (s *spy) WriteHeader(status int) {
r.Status = status s.status = status
r.ResponseWriter.WriteHeader(status) s.ResponseWriter.WriteHeader(status)
} }
func Logger(log zerolog.Logger) server.Middleware { func Logger(l zerolog.Logger) func(http.Handler) http.Handler {
return func(next server.Handler) server.Handler { return func(h http.Handler) http.Handler {
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
traceId := server.GetTraceID(r.Context()) reqID := r.Context().Value(middleware.RequestIDKey).(string)
log.Info(). l.Info().Str("method", r.Method).Str("path", r.URL.Path).Str("rid", reqID).Msg("request received")
Str("trace_id", traceId).
Str("method", r.Method).
Str("path", r.URL.Path).
Str("remove_address", r.RemoteAddr).
Msg("request started")
record := &statusRecorder{ResponseWriter: w, Status: http.StatusOK} s := &spy{ResponseWriter: w}
h.ServeHTTP(s, r)
err := next.ServeHTTP(record, r) l.Info().Str("method", r.Method).Str("path", r.URL.Path).Int("status", s.status).Str("rid", reqID).Msg("request finished")
log.Info().
Str("trace_id", traceId).
Str("method", r.Method).
Str("url", r.URL.Path).
Str("remote_address", r.RemoteAddr).
Int("status_code", record.Status).
Msg("request completed")
return err
})
}
}
func SugarLogger(log zerolog.Logger) server.Middleware {
orange := func(s string) string { return "\033[33m" + s + "\033[0m" }
aqua := func(s string) string { return "\033[36m" + s + "\033[0m" }
red := func(s string) string { return "\033[31m" + s + "\033[0m" }
green := func(s string) string { return "\033[32m" + s + "\033[0m" }
fmtCode := func(code int) string {
switch {
case code >= 500:
return red(fmt.Sprintf("%d", code))
case code >= 400:
return orange(fmt.Sprintf("%d", code))
case code >= 300:
return aqua(fmt.Sprintf("%d", code))
default:
return green(fmt.Sprintf("%d", code))
}
}
bold := func(s string) string { return "\033[1m" + s + "\033[0m" }
atLeast6 := func(s string) string {
for len(s) <= 6 {
s += " "
}
return s
}
return func(next server.Handler) server.Handler {
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
record := &statusRecorder{ResponseWriter: w, Status: http.StatusOK}
err := next.ServeHTTP(record, r) // Blocks until the next handler returns.
url := fmt.Sprintf("%s %s", r.RequestURI, r.Proto)
log.Info().
Str("trace_id", server.GetTraceID(r.Context())).
Msgf("%s %s %s",
bold(fmtCode(record.Status)),
bold(orange(atLeast6(r.Method))),
aqua(url),
)
return err
}) })
} }
} }

View file

@ -1,33 +0,0 @@
package mid
import (
"fmt"
"net/http"
"runtime/debug"
"github.com/hay-kot/homebox/backend/pkgs/server"
)
// Panic is a middleware that recovers from panics anywhere in the chain and wraps the error.
// and returns it up the middleware chain.
func Panic(develop bool) server.Middleware {
return func(h server.Handler) server.Handler {
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (err error) {
defer func() {
if rec := recover(); rec != nil {
trace := debug.Stack()
if develop {
err = fmt.Errorf("PANIC [%v]", rec)
fmt.Printf("%s", string(trace))
} else {
err = fmt.Errorf("PANIC [%v] TRACE[%s]", rec, string(trace))
}
}
}()
return h.ServeHTTP(w, r)
})
}
}

View file

@ -1,8 +0,0 @@
package server
const (
ContentType = "Content-Type"
ContentJSON = "application/json"
ContentXML = "application/xml"
ContentFormUrlEncoded = "application/x-www-form-urlencoded"
)

View file

@ -1,23 +0,0 @@
package server
import "errors"
type shutdownError struct {
message string
}
func (e *shutdownError) Error() string {
return e.message
}
// ShutdownError returns an error that indicates that the server has lost
// integrity and should be shut down.
func ShutdownError(message string) error {
return &shutdownError{message}
}
// IsShutdownError returns true if the error is a shutdown error.
func IsShutdownError(err error) bool {
var e *shutdownError
return errors.As(err, &e)
}

View file

@ -1,25 +0,0 @@
package server
import (
"net/http"
)
type HandlerFunc func(w http.ResponseWriter, r *http.Request) error
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
return f(w, r)
}
type Handler interface {
ServeHTTP(http.ResponseWriter, *http.Request) error
}
// ToHandler converts a function to a customer implementation of the Handler interface.
// that returns an error. This wrapper around the handler function and simply
// returns the nil in all cases
func ToHandler(handler http.Handler) Handler {
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
handler.ServeHTTP(w, r)
return nil
})
}

View file

@ -1,37 +0,0 @@
package server
import (
"net/http"
"strings"
)
type Middleware func(Handler) Handler
// wrapMiddleware creates a new handler by wrapping middleware around a final
// handler. The middlewares' Handlers will be executed by requests in the order
// they are provided.
func wrapMiddleware(mw []Middleware, handler Handler) Handler {
// Loop backwards through the middleware invoking each one. Replace the
// handler with the new wrapped handler. Looping backwards ensures that the
// first middleware of the slice is the first to be executed by requests.
for i := len(mw) - 1; i >= 0; i-- {
h := mw[i]
if h != nil {
handler = h(handler)
}
}
return handler
}
// StripTrailingSlash is a middleware that will strip trailing slashes from the request path.
//
// Example: /api/v1/ -> /api/v1
func StripTrailingSlash() Middleware {
return func(h Handler) Handler {
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
r.URL.Path = strings.TrimSuffix(r.URL.Path, "/")
return h.ServeHTTP(w, r)
})
}
}

View file

@ -1,102 +0,0 @@
package server
import (
"context"
"net/http"
"github.com/google/uuid"
)
type vkey int
const (
// Key is the key for the server in the request context.
key vkey = 1
)
type Values struct {
TraceID string
}
func GetTraceID(ctx context.Context) string {
v, ok := ctx.Value(key).(Values)
if !ok {
return ""
}
return v.TraceID
}
func (s *Server) toHttpHandler(handler Handler, mw ...Middleware) http.HandlerFunc {
handler = wrapMiddleware(mw, handler)
handler = wrapMiddleware(s.mw, handler)
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Add the trace ID to the context
ctx = context.WithValue(ctx, key, Values{
TraceID: uuid.NewString(),
})
err := handler.ServeHTTP(w, r.WithContext(ctx))
if err != nil {
if IsShutdownError(err) {
_ = s.Shutdown("SIGTERM")
}
}
}
}
func (s *Server) handle(method, pattern string, handler Handler, mw ...Middleware) {
h := s.toHttpHandler(handler, mw...)
switch method {
case http.MethodGet:
s.mux.Get(pattern, h)
case http.MethodPost:
s.mux.Post(pattern, h)
case http.MethodPut:
s.mux.Put(pattern, h)
case http.MethodDelete:
s.mux.Delete(pattern, h)
case http.MethodPatch:
s.mux.Patch(pattern, h)
case http.MethodHead:
s.mux.Head(pattern, h)
case http.MethodOptions:
s.mux.Options(pattern, h)
}
}
func (s *Server) Get(pattern string, handler Handler, mw ...Middleware) {
s.handle(http.MethodGet, pattern, handler, mw...)
}
func (s *Server) Post(pattern string, handler Handler, mw ...Middleware) {
s.handle(http.MethodPost, pattern, handler, mw...)
}
func (s *Server) Put(pattern string, handler Handler, mw ...Middleware) {
s.handle(http.MethodPut, pattern, handler, mw...)
}
func (s *Server) Delete(pattern string, handler Handler, mw ...Middleware) {
s.handle(http.MethodDelete, pattern, handler, mw...)
}
func (s *Server) Patch(pattern string, handler Handler, mw ...Middleware) {
s.handle(http.MethodPatch, pattern, handler, mw...)
}
func (s *Server) Head(pattern string, handler Handler, mw ...Middleware) {
s.handle(http.MethodHead, pattern, handler, mw...)
}
func (s *Server) Options(pattern string, handler Handler, mw ...Middleware) {
s.handle(http.MethodOptions, pattern, handler, mw...)
}
func (s *Server) NotFound(handler Handler) {
s.mux.NotFound(s.toHttpHandler(handler))
}

View file

@ -1,48 +0,0 @@
package server
import (
"encoding/json"
"net/http"
)
// Decode reads the body of an HTTP request looking for a JSON document. The
// body is decoded into the provided value.
func Decode(r *http.Request, val interface{}) error {
decoder := json.NewDecoder(r.Body)
// decoder.DisallowUnknownFields()
if err := decoder.Decode(val); err != nil {
return err
}
return nil
}
// GetId is a shortcut to get the id from the request URL or return a default value
func GetParam(r *http.Request, key, d string) string {
val := r.URL.Query().Get(key)
if val == "" {
return d
}
return val
}
// GetSkip is a shortcut to get the skip from the request URL parameters
func GetSkip(r *http.Request, d string) string {
return GetParam(r, "skip", d)
}
// GetSkip is a shortcut to get the skip from the request URL parameters
func GetId(r *http.Request, d string) string {
return GetParam(r, "id", d)
}
// GetLimit is a shortcut to get the limit from the request URL parameters
func GetLimit(r *http.Request, d string) string {
return GetParam(r, "limit", d)
}
// GetQuery is a shortcut to get the sort from the request URL parameters
func GetQuery(r *http.Request, d string) string {
return GetParam(r, "query", d)
}

View file

@ -1,210 +0,0 @@
package server
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
type TestStruct struct {
Name string `json:"name"`
Data string `json:"data"`
}
func TestDecode(t *testing.T) {
type args struct {
r *http.Request
val interface{}
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "check_error",
args: args{
r: &http.Request{
Body: http.NoBody,
},
val: make(map[string]interface{}),
},
wantErr: true,
},
{
name: "check_success",
args: args{
r: httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test","data":"test"}`)),
val: TestStruct{
Name: "test",
Data: "test",
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := Decode(tt.args.r, &tt.args.val); (err != nil) != tt.wantErr {
t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestGetParam(t *testing.T) {
type args struct {
r *http.Request
key string
d string
}
tests := []struct {
name string
args args
want string
}{
{
name: "check_default",
args: args{
r: httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test","data":"test"}`)),
key: "id",
d: "default",
},
want: "default",
},
{
name: "check_id",
args: args{
r: httptest.NewRequest("POST", "/item?id=123", strings.NewReader(`{"name":"test","data":"test"}`)),
key: "id",
d: "",
},
want: "123",
},
{
name: "check_query",
args: args{
r: httptest.NewRequest("POST", "/item?query=hello-world", strings.NewReader(`{"name":"test","data":"test"}`)),
key: "query",
d: "",
},
want: "hello-world",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetParam(tt.args.r, tt.args.key, tt.args.d); got != tt.want {
t.Errorf("GetParam() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetSkip(t *testing.T) {
type args struct {
r *http.Request
d string
}
tests := []struct {
name string
args args
want string
}{
{
name: "check_default",
args: args{
r: httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test","data":"test"}`)),
d: "0",
},
want: "0",
},
{
name: "check_skip",
args: args{
r: httptest.NewRequest("POST", "/item?skip=107", strings.NewReader(`{"name":"test","data":"test"}`)),
d: "0",
},
want: "107",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetSkip(tt.args.r, tt.args.d); got != tt.want {
t.Errorf("GetSkip() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetLimit(t *testing.T) {
type args struct {
r *http.Request
d string
}
tests := []struct {
name string
args args
want string
}{
{
name: "check_default",
args: args{
r: httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test","data":"test"}`)),
d: "0",
},
want: "0",
},
{
name: "check_limit",
args: args{
r: httptest.NewRequest("POST", "/item?limit=107", strings.NewReader(`{"name":"test","data":"test"}`)),
d: "0",
},
want: "107",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetLimit(tt.args.r, tt.args.d); got != tt.want {
t.Errorf("GetLimit() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetQuery(t *testing.T) {
type args struct {
r *http.Request
d string
}
tests := []struct {
name string
args args
want string
}{
{
name: "check_default",
args: args{
r: httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test","data":"test"}`)),
d: "0",
},
want: "0",
},
{
name: "check_query",
args: args{
r: httptest.NewRequest("POST", "/item?query=hello-query", strings.NewReader(`{"name":"test","data":"test"}`)),
d: "0",
},
want: "hello-query",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetQuery(tt.args.r, tt.args.d); got != tt.want {
t.Errorf("GetQuery() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -1,39 +0,0 @@
package server
import (
"encoding/json"
"net/http"
)
type ErrorResponse struct {
Error string `json:"error"`
Fields map[string]string `json:"fields,omitempty"`
}
// Respond converts a Go value to JSON and sends it to the client.
// Adapted from https://github.com/ardanlabs/service/tree/master/foundation/web
func Respond(w http.ResponseWriter, statusCode int, data interface{}) error {
if statusCode == http.StatusNoContent {
w.WriteHeader(statusCode)
return nil
}
// Convert the response value to JSON.
jsonData, err := json.Marshal(data)
if err != nil {
panic(err)
}
// Set the content type and headers once we know marshaling has succeeded.
w.Header().Set("Content-Type", ContentJSON)
// Write the status code to the response.
w.WriteHeader(statusCode)
// Send the result back to the client.
if _, err := w.Write(jsonData); err != nil {
return err
}
return nil
}

View file

@ -1,40 +0,0 @@
package server
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Respond_NoContent(t *testing.T) {
recorder := httptest.NewRecorder()
dummystruct := struct {
Name string
}{
Name: "dummy",
}
err := Respond(recorder, http.StatusNoContent, dummystruct)
assert.NoError(t, err)
assert.Equal(t, http.StatusNoContent, recorder.Code)
assert.Empty(t, recorder.Body.String())
}
func Test_Respond_JSON(t *testing.T) {
recorder := httptest.NewRecorder()
dummystruct := struct {
Name string `json:"name"`
}{
Name: "dummy",
}
err := Respond(recorder, http.StatusCreated, dummystruct)
assert.NoError(t, err)
assert.Equal(t, http.StatusCreated, recorder.Code)
assert.JSONEq(t, recorder.Body.String(), `{"name":"dummy"}`)
assert.Equal(t, "application/json", recorder.Header().Get("Content-Type"))
}

View file

@ -1,19 +0,0 @@
package server
type Result struct {
Error bool `json:"error,omitempty"`
Details interface{} `json:"details,omitempty"`
Message string `json:"message,omitempty"`
Item interface{} `json:"item,omitempty"`
}
type Results struct {
Items any `json:"items"`
}
// Wrap creates a Wrapper instance and adds the initial namespace and data to be returned.
func Wrap(data interface{}) Result {
return Result{
Item: data,
}
}

View file

@ -1,144 +0,0 @@
package server
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/go-chi/chi/v5"
)
var (
ErrServerNotStarted = errors.New("server not started")
ErrServerAlreadyStarted = errors.New("server already started")
)
type Server struct {
Host string
Port string
Worker Worker
wg sync.WaitGroup
mux *chi.Mux
// mw is the global middleware chain for the server.
mw []Middleware
started bool
activeServer *http.Server
idleTimeout time.Duration
readTimeout time.Duration
writeTimeout time.Duration
}
func NewServer(opts ...Option) *Server {
s := &Server{
Host: "localhost",
Port: "8080",
mux: chi.NewRouter(),
Worker: NewSimpleWorker(),
idleTimeout: 30 * time.Second,
readTimeout: 10 * time.Second,
writeTimeout: 10 * time.Second,
}
for _, opt := range opts {
err := opt(s)
if err != nil {
panic(err)
}
}
return s
}
func (s *Server) Shutdown(sig string) error {
if !s.started {
return ErrServerNotStarted
}
fmt.Printf("Received %s signal, shutting down\n", sig)
// Create a context with a 5-second timeout.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := s.activeServer.Shutdown(ctx)
s.started = false
if err != nil {
return err
}
fmt.Println("Http server shutdown, waiting for all tasks to finish")
s.wg.Wait()
return nil
}
func (s *Server) Start() error {
if s.started {
return ErrServerAlreadyStarted
}
s.activeServer = &http.Server{
Addr: s.Host + ":" + s.Port,
Handler: s.mux,
IdleTimeout: s.idleTimeout,
ReadTimeout: s.readTimeout,
WriteTimeout: s.writeTimeout,
}
shutdownError := make(chan error)
go func() {
// Create a quit channel which carries os.Signal values.
quit := make(chan os.Signal, 1)
// Use signal.Notify() to listen for incoming SIGINT and SIGTERM signals and
// relay them to the quit channel.
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// Read the signal from the quit channel. block until received
sig := <-quit
err := s.Shutdown(sig.String())
if err != nil {
shutdownError <- err
}
// Exit the application with a 0 (success) status code.
os.Exit(0)
}()
s.started = true
err := s.activeServer.ListenAndServe()
if !errors.Is(err, http.ErrServerClosed) {
return err
}
err = <-shutdownError
if err != nil {
return err
}
fmt.Println("Server shutdown successfully")
return nil
}
// Background starts a go routine that runs on the servers pool. In the event of a shutdown
// request, the server will wait until all open goroutines have finished before shutting down.
func (svr *Server) Background(task func()) {
svr.wg.Add(1)
svr.Worker.Add(func() {
defer svr.wg.Done()
task()
})
}

View file

@ -1,54 +0,0 @@
package server
import "time"
type Option = func(s *Server) error
func WithMiddleware(mw ...Middleware) Option {
return func(s *Server) error {
s.mw = append(s.mw, mw...)
return nil
}
}
func WithWorker(w Worker) Option {
return func(s *Server) error {
s.Worker = w
return nil
}
}
func WithHost(host string) Option {
return func(s *Server) error {
s.Host = host
return nil
}
}
func WithPort(port string) Option {
return func(s *Server) error {
s.Port = port
return nil
}
}
func WithReadTimeout(seconds int) Option {
return func(s *Server) error {
s.readTimeout = time.Duration(seconds) * time.Second
return nil
}
}
func WithWriteTimeout(seconds int) Option {
return func(s *Server) error {
s.writeTimeout = time.Duration(seconds) * time.Second
return nil
}
}
func WithIdleTimeout(seconds int) Option {
return func(s *Server) error {
s.idleTimeout = time.Duration(seconds) * time.Second
return nil
}
}

View file

@ -1,101 +0,0 @@
package server
import (
"net/http"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func testServer(t *testing.T, r http.Handler) *Server {
svr := NewServer(WithHost("127.0.0.1"), WithPort("19245"))
if r != nil {
svr.mux.Mount("/", r)
}
go func() {
err := svr.Start()
assert.NoError(t, err)
}()
ping := func() error {
_, err := http.Get("http://127.0.0.1:19245")
return err
}
for {
if err := ping(); err == nil {
break
}
time.Sleep(time.Millisecond * 100)
}
return svr
}
func Test_ServerShutdown_Error(t *testing.T) {
svr := NewServer(WithHost("127.0.0.1"), WithPort("19245"))
err := svr.Shutdown("test")
assert.ErrorIs(t, err, ErrServerNotStarted)
}
func Test_ServerStarts_Error(t *testing.T) {
svr := testServer(t, nil)
err := svr.Start()
assert.ErrorIs(t, err, ErrServerAlreadyStarted)
err = svr.Shutdown("test")
assert.NoError(t, err)
}
func Test_ServerStarts(t *testing.T) {
svr := testServer(t, nil)
err := svr.Shutdown("test")
assert.NoError(t, err)
}
func Test_GracefulServerShutdownWithWorkers(t *testing.T) {
isFinished := false
svr := testServer(t, nil)
svr.Background(func() {
time.Sleep(time.Second * 4)
isFinished = true
})
err := svr.Shutdown("test")
assert.NoError(t, err)
assert.True(t, isFinished)
}
func Test_GracefulServerShutdownWithRequests(t *testing.T) {
var isFinished atomic.Bool
router := http.NewServeMux()
// add long running handler func
router.HandleFunc("/test", func(rw http.ResponseWriter, r *http.Request) {
time.Sleep(time.Second * 3)
isFinished.Store(true)
})
svr := testServer(t, router)
// Make request to "/test"
go func() {
_, _ = http.Get("http://127.0.0.1:19245/test") // This is probably bad?
}()
time.Sleep(time.Second) // Hack to wait for the request to be made
err := svr.Shutdown("test")
assert.NoError(t, err)
assert.True(t, isFinished.Load())
}

View file

@ -1,21 +0,0 @@
package server
// TODO: #2 Implement Go routine pool/job queue
type Worker interface {
Add(func())
}
// SimpleWorker is a simple background worker that implements
// the Worker interface and runs all tasks in a go routine without
// a pool or que or limits. It's useful for simple or small applications
// with minimal/short background tasks
type SimpleWorker struct{}
func NewSimpleWorker() *SimpleWorker {
return &SimpleWorker{}
}
func (sw *SimpleWorker) Add(task func()) {
go task()
}

View file

@ -417,8 +417,8 @@
} }
], ],
"responses": { "responses": {
"200": { "201": {
"description": "OK", "description": "Created",
"schema": { "schema": {
"$ref": "#/definitions/repo.ItemSummary" "$ref": "#/definitions/repo.ItemSummary"
} }
@ -686,7 +686,7 @@
"422": { "422": {
"description": "Unprocessable Entity", "description": "Unprocessable Entity",
"schema": { "schema": {
"$ref": "#/definitions/server.ErrorResponse" "$ref": "#/definitions/mid.ErrorResponse"
} }
} }
} }
@ -856,8 +856,8 @@
} }
], ],
"responses": { "responses": {
"200": { "201": {
"description": "OK", "description": "Created",
"schema": { "schema": {
"$ref": "#/definitions/repo.MaintenanceEntry" "$ref": "#/definitions/repo.MaintenanceEntry"
} }
@ -939,7 +939,7 @@
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Results" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1111,7 +1111,7 @@
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Results" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1191,7 +1191,7 @@
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Results" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1331,7 +1331,7 @@
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Results" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1711,7 +1711,7 @@
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Result" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1756,7 +1756,7 @@
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/server.Result" "$ref": "#/definitions/v1.Wrapped"
}, },
{ {
"type": "object", "type": "object",
@ -1793,6 +1793,20 @@
} }
}, },
"definitions": { "definitions": {
"mid.ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"fields": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"repo.DocumentOut": { "repo.DocumentOut": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1894,9 +1908,15 @@
}, },
"repo.ItemCreate": { "repo.ItemCreate": {
"type": "object", "type": "object",
"required": [
"description",
"name"
],
"properties": { "properties": {
"description": { "description": {
"type": "string" "type": "string",
"maxLength": 1000,
"minLength": 1
}, },
"labelIds": { "labelIds": {
"type": "array", "type": "array",
@ -1909,7 +1929,9 @@
"type": "string" "type": "string"
}, },
"name": { "name": {
"type": "string" "type": "string",
"maxLength": 255,
"minLength": 1
}, },
"parentId": { "parentId": {
"type": "string", "type": "string",
@ -2200,15 +2222,21 @@
}, },
"repo.LabelCreate": { "repo.LabelCreate": {
"type": "object", "type": "object",
"required": [
"name"
],
"properties": { "properties": {
"color": { "color": {
"type": "string" "type": "string"
}, },
"description": { "description": {
"type": "string" "type": "string",
"maxLength": 255
}, },
"name": { "name": {
"type": "string" "type": "string",
"maxLength": 255,
"minLength": 1
} }
} }
}, },
@ -2655,39 +2683,6 @@
} }
} }
}, },
"server.ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"fields": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"server.Result": {
"type": "object",
"properties": {
"details": {},
"error": {
"type": "boolean"
},
"item": {},
"message": {
"type": "string"
}
}
},
"server.Results": {
"type": "object",
"properties": {
"items": {}
}
},
"services.UserRegistration": { "services.UserRegistration": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -2783,12 +2778,17 @@
}, },
"v1.GroupInvitationCreate": { "v1.GroupInvitationCreate": {
"type": "object", "type": "object",
"required": [
"uses"
],
"properties": { "properties": {
"expiresAt": { "expiresAt": {
"type": "string" "type": "string"
}, },
"uses": { "uses": {
"type": "integer" "type": "integer",
"maximum": 100,
"minimum": 1
} }
} }
}, },
@ -2813,6 +2813,12 @@
"type": "string" "type": "string"
} }
} }
},
"v1.Wrapped": {
"type": "object",
"properties": {
"item": {}
}
} }
}, },
"securityDefinitions": { "securityDefinitions": {

View file

@ -10,6 +10,11 @@
* --------------------------------------------------------------- * ---------------------------------------------------------------
*/ */
export interface MidErrorResponse {
error: string;
fields: Record<string, string>;
}
export interface DocumentOut { export interface DocumentOut {
id: string; id: string;
path: string; path: string;
@ -52,10 +57,18 @@ export interface ItemAttachmentUpdate {
} }
export interface ItemCreate { export interface ItemCreate {
/**
* @minLength 1
* @maxLength 1000
*/
description: string; description: string;
labelIds: string[]; labelIds: string[];
/** Edges */ /** Edges */
locationId: string; locationId: string;
/**
* @minLength 1
* @maxLength 255
*/
name: string; name: string;
parentId: string | null; parentId: string | null;
} }
@ -164,7 +177,12 @@ export interface ItemUpdate {
export interface LabelCreate { export interface LabelCreate {
color: string; color: string;
/** @maxLength 255 */
description: string; description: string;
/**
* @minLength 1
* @maxLength 255
*/
name: string; name: string;
} }
@ -346,22 +364,6 @@ export interface ValueOverTimeEntry {
value: number; value: number;
} }
export interface ServerErrorResponse {
error: string;
fields: Record<string, string>;
}
export interface ServerResult {
details: any;
error: boolean;
item: any;
message: string;
}
export interface ServerResults {
items: any;
}
export interface UserRegistration { export interface UserRegistration {
email: string; email: string;
name: string; name: string;
@ -402,6 +404,10 @@ export interface GroupInvitation {
export interface GroupInvitationCreate { export interface GroupInvitationCreate {
expiresAt: Date | string; expiresAt: Date | string;
/**
* @min 1
* @max 100
*/
uses: number; uses: number;
} }
@ -414,3 +420,7 @@ export interface TokenResponse {
expiresAt: Date | string; expiresAt: Date | string;
token: string; token: string;
} }
export interface Wrapped {
item: any;
}