forked from mirrors/homebox
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:
parent
184b494fc3
commit
db80f8a159
56 changed files with 806 additions and 1947 deletions
46
Taskfile.yml
46
Taskfile.yml
|
@ -27,47 +27,47 @@ tasks:
|
|||
--modular \
|
||||
--path ./backend/app/api/static/docs/swagger.json \
|
||||
--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
|
||||
sources:
|
||||
- "./backend/app/api/**/*"
|
||||
- "./backend/internal/data/**"
|
||||
- "./backend/internal/services/**/*"
|
||||
- "./scripts/process-types.py"
|
||||
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"
|
||||
- "./backend/internal/core/services/**/*"
|
||||
- "./backend/app/tools/typegen/main.go"
|
||||
|
||||
go:run:
|
||||
desc: Starts the backend api server (depends on generate task)
|
||||
dir: backend
|
||||
deps:
|
||||
- generate
|
||||
cmds:
|
||||
- cd backend && go run ./app/api/ {{ .CLI_ARGS }}
|
||||
- go run ./app/api/ {{ .CLI_ARGS }}
|
||||
silent: false
|
||||
|
||||
go:test:
|
||||
desc: Runs all go tests using gotestsum - supports passing gotestsum args
|
||||
dir: backend
|
||||
cmds:
|
||||
- cd backend && gotestsum {{ .CLI_ARGS }} ./...
|
||||
- gotestsum {{ .CLI_ARGS }} ./...
|
||||
|
||||
go:coverage:
|
||||
desc: Runs all go tests with -race flag and generates a coverage report
|
||||
dir: backend
|
||||
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
|
||||
|
||||
go:tidy:
|
||||
desc: Runs go mod tidy on the backend
|
||||
dir: backend
|
||||
cmds:
|
||||
- cd backend && go mod tidy
|
||||
- go mod tidy
|
||||
|
||||
go:lint:
|
||||
desc: Runs golangci-lint
|
||||
dir: backend
|
||||
cmds:
|
||||
- cd backend && golangci-lint run ./...
|
||||
- golangci-lint run ./...
|
||||
|
||||
go:all:
|
||||
desc: Runs all go test and lint related tasks
|
||||
|
@ -78,19 +78,19 @@ tasks:
|
|||
|
||||
go:build:
|
||||
desc: Builds the backend binary
|
||||
dir: backend
|
||||
cmds:
|
||||
- cd backend && go build -o ../build/backend ./app/api
|
||||
- go build -o ../build/backend ./app/api
|
||||
|
||||
db:generate:
|
||||
desc: Run Entgo.io Code Generation
|
||||
dir: backend/internal/
|
||||
cmds:
|
||||
- |
|
||||
cd backend/internal/ && go generate ./... \
|
||||
go generate ./... \
|
||||
--template=./data/ent/schema/templates/has_id.tmpl
|
||||
sources:
|
||||
- "./backend/internal/data/ent/schema/**/*"
|
||||
generates:
|
||||
- "./backend/internal/ent/"
|
||||
|
||||
db:migration:
|
||||
desc: Runs the database diff engine to generate a SQL migration files
|
||||
|
@ -101,23 +101,27 @@ tasks:
|
|||
|
||||
ui:watch:
|
||||
desc: Starts the vitest test runner in watch mode
|
||||
dir: frontend
|
||||
cmds:
|
||||
- cd frontend && pnpm run test:watch
|
||||
- pnpm run test:watch
|
||||
|
||||
ui:dev:
|
||||
desc: Run frontend development server
|
||||
dir: frontend
|
||||
cmds:
|
||||
- cd frontend && pnpm dev
|
||||
- pnpm dev
|
||||
|
||||
ui:fix:
|
||||
desc: Runs prettier and eslint on the frontend
|
||||
dir: frontend
|
||||
cmds:
|
||||
- cd frontend && pnpm run lint:fix
|
||||
- pnpm run lint:fix
|
||||
|
||||
ui:check:
|
||||
desc: Runs type checking
|
||||
dir: frontend
|
||||
cmds:
|
||||
- cd frontend && pnpm run typecheck
|
||||
- pnpm run typecheck
|
||||
|
||||
test:ci:
|
||||
desc: Runs end-to-end test on a live server (only for use in CI)
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/hay-kot/homebox/backend/internal/data/repo"
|
||||
"github.com/hay-kot/homebox/backend/internal/sys/config"
|
||||
"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 {
|
||||
|
|
|
@ -5,9 +5,18 @@ import (
|
|||
|
||||
"github.com/hay-kot/homebox/backend/internal/core/services"
|
||||
"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) {
|
||||
return func(ctrl *V1Controller) {
|
||||
ctrl.maxUploadSize = maxUploadSize
|
||||
|
@ -81,9 +90,9 @@ func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, options ..
|
|||
// @Produce json
|
||||
// @Success 200 {object} ApiSummary
|
||||
// @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 server.Respond(w, http.StatusOK, ApiSummary{
|
||||
return server.JSON(w, http.StatusOK, ApiSummary{
|
||||
Healthy: ready(),
|
||||
Title: "Homebox",
|
||||
Message: "Track, Manage, and Organize your shit",
|
||||
|
|
|
@ -7,7 +7,8 @@ import (
|
|||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/core/services"
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -15,7 +16,7 @@ type ActionAmountResult struct {
|
|||
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 {
|
||||
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 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
|
||||
// @Router /v1/actions/ensure-asset-ids [Post]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleEnsureAssetID() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleEnsureAssetID() errchain.HandlerFunc {
|
||||
return actionHandlerFactory("ensure asset IDs", ctrl.svc.Items.EnsureAssetID)
|
||||
}
|
||||
|
||||
|
@ -51,7 +52,7 @@ func (ctrl *V1Controller) HandleEnsureAssetID() server.HandlerFunc {
|
|||
// @Success 200 {object} ActionAmountResult
|
||||
// @Router /v1/actions/ensure-import-refs [Post]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleEnsureImportRefs() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleEnsureImportRefs() errchain.HandlerFunc {
|
||||
return actionHandlerFactory("ensure import refs", ctrl.svc.Items.EnsureImportRef)
|
||||
}
|
||||
|
||||
|
@ -64,6 +65,6 @@ func (ctrl *V1Controller) HandleEnsureImportRefs() server.HandlerFunc {
|
|||
// @Success 200 {object} ActionAmountResult
|
||||
// @Router /v1/actions/zero-item-time-fields [Post]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemDateZeroOut() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleItemDateZeroOut() errchain.HandlerFunc {
|
||||
return actionHandlerFactory("zero out date time", ctrl.repo.Items.ZeroOutTimeFields)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@ import (
|
|||
"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/pkgs/server"
|
||||
"github.com/hay-kot/safeserve/errchain"
|
||||
"github.com/hay-kot/safeserve/server"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
@ -23,7 +24,7 @@ import (
|
|||
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
|
||||
// @Router /v1/assets/{id} [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleAssetGet() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleAssetGet() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx := services.NewContext(r.Context())
|
||||
assetIdParam := chi.URLParam(r, "id")
|
||||
|
@ -38,7 +39,7 @@ func (ctrl *V1Controller) HandleAssetGet() server.HandlerFunc {
|
|||
if pageParam != "" {
|
||||
page, err = strconv.ParseInt(pageParam, 10, 64)
|
||||
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 != "" {
|
||||
pageSize, err = strconv.ParseInt(pageSizeParam, 10, 64)
|
||||
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")
|
||||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||
}
|
||||
return server.Respond(w, http.StatusOK, items)
|
||||
return server.JSON(w, http.StatusOK, items)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,8 @@ import (
|
|||
|
||||
"github.com/hay-kot/homebox/backend/internal/core/services"
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -36,26 +37,27 @@ type (
|
|||
// @Produce json
|
||||
// @Success 200 {object} TokenResponse
|
||||
// @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 {
|
||||
loginForm := &LoginForm{}
|
||||
|
||||
switch r.Header.Get("Content-Type") {
|
||||
case server.ContentFormUrlEncoded:
|
||||
case "application/x-www-form-urlencoded":
|
||||
err := r.ParseForm()
|
||||
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.Password = r.PostFormValue("password")
|
||||
case server.ContentJSON:
|
||||
case "application/json":
|
||||
err := server.Decode(r, loginForm)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to decode login form")
|
||||
return errors.New("failed to decode login form")
|
||||
}
|
||||
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 == "" {
|
||||
|
@ -76,7 +78,7 @@ func (ctrl *V1Controller) HandleAuthLogin() server.HandlerFunc {
|
|||
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,
|
||||
ExpiresAt: newToken.ExpiresAt,
|
||||
AttachmentToken: newToken.AttachmentToken,
|
||||
|
@ -91,7 +93,7 @@ func (ctrl *V1Controller) HandleAuthLogin() server.HandlerFunc {
|
|||
// @Success 204
|
||||
// @Router /v1/users/logout [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleAuthLogout() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleAuthLogout() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
token := services.UseTokenCtx(r.Context())
|
||||
if token == "" {
|
||||
|
@ -103,7 +105,7 @@ func (ctrl *V1Controller) HandleAuthLogout() server.HandlerFunc {
|
|||
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
|
||||
// @Router /v1/users/refresh [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleAuthRefresh() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleAuthRefresh() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
requestToken := services.UseTokenCtx(r.Context())
|
||||
if requestToken == "" {
|
||||
|
@ -128,6 +130,6 @@ func (ctrl *V1Controller) HandleAuthRefresh() server.HandlerFunc {
|
|||
return validate.NewUnauthorizedError()
|
||||
}
|
||||
|
||||
return server.Respond(w, http.StatusOK, newToken)
|
||||
return server.JSON(w, http.StatusOK, newToken)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,14 +6,13 @@ import (
|
|||
|
||||
"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/pkgs/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/hay-kot/homebox/backend/internal/web/adapters"
|
||||
"github.com/hay-kot/safeserve/errchain"
|
||||
)
|
||||
|
||||
type (
|
||||
GroupInvitationCreate struct {
|
||||
Uses int `json:"uses"`
|
||||
Uses int `json:"uses" validate:"required,min=1,max=100"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
}
|
||||
|
||||
|
@ -32,8 +31,13 @@ type (
|
|||
// @Success 200 {object} repo.Group
|
||||
// @Router /v1/groups [Get]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleGroupGet() server.HandlerFunc {
|
||||
return ctrl.handleGroupGeneral()
|
||||
func (ctrl *V1Controller) HandleGroupGet() errchain.HandlerFunc {
|
||||
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
|
||||
|
@ -45,41 +49,13 @@ func (ctrl *V1Controller) HandleGroupGet() server.HandlerFunc {
|
|||
// @Success 200 {object} repo.Group
|
||||
// @Router /v1/groups [Put]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleGroupUpdate() server.HandlerFunc {
|
||||
return ctrl.handleGroupGeneral()
|
||||
}
|
||||
|
||||
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
|
||||
func (ctrl *V1Controller) HandleGroupUpdate() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request, body repo.GroupUpdate) (repo.Group, error) {
|
||||
auth := services.NewContext(r.Context())
|
||||
return ctrl.svc.Group.UpdateGroup(auth, body)
|
||||
}
|
||||
|
||||
return adapters.Action(fn, http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleGroupInvitationsCreate godoc
|
||||
|
@ -91,30 +67,22 @@ func (ctrl *V1Controller) handleGroupGeneral() server.HandlerFunc {
|
|||
// @Success 200 {object} GroupInvitation
|
||||
// @Router /v1/groups/invitations [Post]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleGroupInvitationsCreate() server.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
data := GroupInvitationCreate{}
|
||||
if err := server.Decode(r, &data); err != nil {
|
||||
log.Err(err).Msg("failed to decode user registration data")
|
||||
return validate.NewRequestError(err, http.StatusBadRequest)
|
||||
func (ctrl *V1Controller) HandleGroupInvitationsCreate() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request, body GroupInvitationCreate) (GroupInvitation, error) {
|
||||
if body.ExpiresAt.IsZero() {
|
||||
body.ExpiresAt = time.Now().Add(time.Hour * 24)
|
||||
}
|
||||
|
||||
if data.ExpiresAt.IsZero() {
|
||||
data.ExpiresAt = time.Now().Add(time.Hour * 24)
|
||||
}
|
||||
auth := services.NewContext(r.Context())
|
||||
|
||||
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)
|
||||
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{
|
||||
return GroupInvitation{
|
||||
Token: token,
|
||||
ExpiresAt: data.ExpiresAt,
|
||||
Uses: data.Uses,
|
||||
})
|
||||
ExpiresAt: body.ExpiresAt,
|
||||
Uses: body.Uses,
|
||||
}, err
|
||||
}
|
||||
|
||||
return adapters.Action(fn, http.StatusCreated)
|
||||
}
|
||||
|
|
|
@ -7,10 +7,13 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"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/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"
|
||||
)
|
||||
|
||||
|
@ -27,7 +30,7 @@ import (
|
|||
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
|
||||
// @Router /v1/items [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemsGetAll() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
|
||||
extractQuery := func(r *http.Request) repo.ItemQuery {
|
||||
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))
|
||||
if err != nil {
|
||||
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{},
|
||||
})
|
||||
}
|
||||
log.Err(err).Msg("failed to get items")
|
||||
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
|
||||
// @Produce json
|
||||
// @Param payload body repo.ItemCreate true "Item Data"
|
||||
// @Success 200 {object} repo.ItemSummary
|
||||
// @Success 201 {object} repo.ItemSummary
|
||||
// @Router /v1/items [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemsCreate() server.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
createData := repo.ItemCreate{}
|
||||
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)
|
||||
func (ctrl *V1Controller) HandleItemsCreate() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request, body repo.ItemCreate) (repo.ItemOut, error) {
|
||||
return ctrl.svc.Items.Create(services.NewContext(r.Context()), body)
|
||||
}
|
||||
|
||||
return adapters.Action(fn, http.StatusCreated)
|
||||
}
|
||||
|
||||
// HandleItemGet godocs
|
||||
|
@ -124,8 +116,14 @@ func (ctrl *V1Controller) HandleItemsCreate() server.HandlerFunc {
|
|||
// @Success 200 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id} [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemGet() server.HandlerFunc {
|
||||
return ctrl.handleItemsGeneral()
|
||||
func (ctrl *V1Controller) HandleItemGet() errchain.HandlerFunc {
|
||||
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
|
||||
|
@ -137,8 +135,14 @@ func (ctrl *V1Controller) HandleItemGet() server.HandlerFunc {
|
|||
// @Success 204
|
||||
// @Router /v1/items/{id} [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemDelete() server.HandlerFunc {
|
||||
return ctrl.handleItemsGeneral()
|
||||
func (ctrl *V1Controller) HandleItemDelete() errchain.HandlerFunc {
|
||||
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
|
||||
|
@ -151,50 +155,15 @@ func (ctrl *V1Controller) HandleItemDelete() server.HandlerFunc {
|
|||
// @Success 200 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id} [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemUpdate() server.HandlerFunc {
|
||||
return ctrl.handleItemsGeneral()
|
||||
}
|
||||
func (ctrl *V1Controller) HandleItemUpdate() errchain.HandlerFunc {
|
||||
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 {
|
||||
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:
|
||||
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
|
||||
body.ID = ID
|
||||
return ctrl.repo.Items.UpdateByGroup(auth, auth.GID, body)
|
||||
}
|
||||
|
||||
return adapters.ActionID("id", fn, http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleGetAllCustomFieldNames godocs
|
||||
|
@ -206,17 +175,13 @@ func (ctrl *V1Controller) handleItemsGeneral() server.HandlerFunc {
|
|||
// @Router /v1/items/fields [GET]
|
||||
// @Success 200 {object} []string
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleGetAllCustomFieldNames() server.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx := services.NewContext(r.Context())
|
||||
|
||||
v, err := ctrl.repo.Items.GetAllCustomFieldNames(r.Context(), ctx.GID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return server.Respond(w, http.StatusOK, v)
|
||||
func (ctrl *V1Controller) HandleGetAllCustomFieldNames() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request) ([]string, error) {
|
||||
auth := services.NewContext(r.Context())
|
||||
return ctrl.repo.Items.GetAllCustomFieldNames(auth, auth.GID)
|
||||
}
|
||||
|
||||
return adapters.Command(fn, http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleGetAllCustomFieldValues godocs
|
||||
|
@ -228,17 +193,18 @@ func (ctrl *V1Controller) HandleGetAllCustomFieldNames() server.HandlerFunc {
|
|||
// @Router /v1/items/fields/values [GET]
|
||||
// @Success 200 {object} []string
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleGetAllCustomFieldValues() server.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx := services.NewContext(r.Context())
|
||||
|
||||
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)
|
||||
func (ctrl *V1Controller) HandleGetAllCustomFieldValues() errchain.HandlerFunc {
|
||||
type query struct {
|
||||
Field string `schema:"field" validate:"required"`
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -250,7 +216,7 @@ func (ctrl *V1Controller) HandleGetAllCustomFieldValues() server.HandlerFunc {
|
|||
// @Param csv formData file true "Image to upload"
|
||||
// @Router /v1/items/import [Post]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemsImport() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleItemsImport() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
|
||||
if err != nil {
|
||||
|
@ -272,7 +238,7 @@ func (ctrl *V1Controller) HandleItemsImport() server.HandlerFunc {
|
|||
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"
|
||||
// @Router /v1/items/export [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemsExport() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleItemsExport() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx := services.NewContext(r.Context())
|
||||
|
||||
|
|
|
@ -8,7 +8,8 @@ import (
|
|||
"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/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"
|
||||
)
|
||||
|
||||
|
@ -28,10 +29,10 @@ type (
|
|||
// @Param type formData string true "Type of file"
|
||||
// @Param name formData string true "name of the file including extension"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Failure 422 {object} server.ErrorResponse
|
||||
// @Failure 422 {object} mid.ErrorResponse
|
||||
// @Router /v1/items/{id}/attachments [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemAttachmentCreate() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleItemAttachmentCreate() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
|
||||
if err != nil {
|
||||
|
@ -61,7 +62,7 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() server.HandlerFunc {
|
|||
}
|
||||
|
||||
if !errs.Nil() {
|
||||
return server.Respond(w, http.StatusUnprocessableEntity, errs)
|
||||
return server.JSON(w, http.StatusUnprocessableEntity, errs)
|
||||
}
|
||||
|
||||
attachmentType := r.FormValue("type")
|
||||
|
@ -88,7 +89,7 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() server.HandlerFunc {
|
|||
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
|
||||
// @Router /v1/items/{id}/attachments/{attachment_id} [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemAttachmentGet() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleItemAttachmentGet() errchain.HandlerFunc {
|
||||
return ctrl.handleItemAttachmentsHandler
|
||||
}
|
||||
|
||||
|
@ -115,7 +116,7 @@ func (ctrl *V1Controller) HandleItemAttachmentGet() server.HandlerFunc {
|
|||
// @Success 204
|
||||
// @Router /v1/items/{id}/attachments/{attachment_id} [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemAttachmentDelete() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleItemAttachmentDelete() errchain.HandlerFunc {
|
||||
return ctrl.handleItemAttachmentsHandler
|
||||
}
|
||||
|
||||
|
@ -129,7 +130,7 @@ func (ctrl *V1Controller) HandleItemAttachmentDelete() server.HandlerFunc {
|
|||
// @Success 200 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id}/attachments/{attachment_id} [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemAttachmentUpdate() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleItemAttachmentUpdate() errchain.HandlerFunc {
|
||||
return ctrl.handleItemAttachmentsHandler
|
||||
}
|
||||
|
||||
|
@ -164,7 +165,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
|
|||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return server.Respond(w, http.StatusNoContent, nil)
|
||||
return server.JSON(w, http.StatusNoContent, nil)
|
||||
|
||||
// Update Attachment Handler
|
||||
case http.MethodPut:
|
||||
|
@ -182,7 +183,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
|
|||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return server.Respond(w, http.StatusOK, val)
|
||||
return server.JSON(w, http.StatusOK, val)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -3,12 +3,11 @@ package v1
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"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/sys/validate"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/hay-kot/homebox/backend/internal/web/adapters"
|
||||
"github.com/hay-kot/safeserve/errchain"
|
||||
)
|
||||
|
||||
// HandleLabelsGetAll godoc
|
||||
|
@ -16,19 +15,16 @@ import (
|
|||
// @Summary Get All Labels
|
||||
// @Tags Labels
|
||||
// @Produce json
|
||||
// @Success 200 {object} server.Results{items=[]repo.LabelOut}
|
||||
// @Success 200 {object} Wrapped{items=[]repo.LabelOut}
|
||||
// @Router /v1/labels [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLabelsGetAll() server.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
user := services.UseUserCtx(r.Context())
|
||||
labels, err := ctrl.repo.Labels.GetAll(r.Context(), user.GroupID)
|
||||
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})
|
||||
func (ctrl *V1Controller) HandleLabelsGetAll() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request) ([]repo.LabelSummary, error) {
|
||||
auth := services.NewContext(r.Context())
|
||||
return ctrl.repo.Labels.GetAll(auth, auth.GID)
|
||||
}
|
||||
|
||||
return adapters.Command(fn, http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleLabelsCreate godoc
|
||||
|
@ -40,23 +36,13 @@ func (ctrl *V1Controller) HandleLabelsGetAll() server.HandlerFunc {
|
|||
// @Success 200 {object} repo.LabelSummary
|
||||
// @Router /v1/labels [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLabelsCreate() server.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
createData := repo.LabelCreate{}
|
||||
if err := server.Decode(r, &createData); err != nil {
|
||||
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)
|
||||
func (ctrl *V1Controller) HandleLabelsCreate() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request, data repo.LabelCreate) (repo.LabelOut, error) {
|
||||
auth := services.NewContext(r.Context())
|
||||
return ctrl.repo.Labels.Create(auth, auth.GID, data)
|
||||
}
|
||||
|
||||
return adapters.Action(fn, http.StatusCreated)
|
||||
}
|
||||
|
||||
// HandleLabelDelete godocs
|
||||
|
@ -68,8 +54,14 @@ func (ctrl *V1Controller) HandleLabelsCreate() server.HandlerFunc {
|
|||
// @Success 204
|
||||
// @Router /v1/labels/{id} [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLabelDelete() server.HandlerFunc {
|
||||
return ctrl.handleLabelsGeneral()
|
||||
func (ctrl *V1Controller) HandleLabelDelete() errchain.HandlerFunc {
|
||||
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
|
||||
|
@ -81,8 +73,13 @@ func (ctrl *V1Controller) HandleLabelDelete() server.HandlerFunc {
|
|||
// @Success 200 {object} repo.LabelOut
|
||||
// @Router /v1/labels/{id} [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLabelGet() server.HandlerFunc {
|
||||
return ctrl.handleLabelsGeneral()
|
||||
func (ctrl *V1Controller) HandleLabelGet() errchain.HandlerFunc {
|
||||
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
|
||||
|
@ -94,55 +91,12 @@ func (ctrl *V1Controller) HandleLabelGet() server.HandlerFunc {
|
|||
// @Success 200 {object} repo.LabelOut
|
||||
// @Router /v1/labels/{id} [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLabelUpdate() server.HandlerFunc {
|
||||
return ctrl.handleLabelsGeneral()
|
||||
}
|
||||
|
||||
func (ctrl *V1Controller) handleLabelsGeneral() server.HandlerFunc {
|
||||
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
|
||||
func (ctrl *V1Controller) HandleLabelUpdate() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request, ID uuid.UUID, data repo.LabelUpdate) (repo.LabelOut, error) {
|
||||
auth := services.NewContext(r.Context())
|
||||
data.ID = ID
|
||||
return ctrl.repo.Labels.UpdateByGroup(auth, auth.GID, data)
|
||||
}
|
||||
|
||||
return adapters.ActionID("id", fn, http.StatusOK)
|
||||
}
|
||||
|
|
|
@ -3,77 +3,50 @@ package v1
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"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/sys/validate"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/hay-kot/homebox/backend/internal/web/adapters"
|
||||
"github.com/hay-kot/safeserve/errchain"
|
||||
)
|
||||
|
||||
// HandleLocationTreeQuery godoc
|
||||
// HandleLocationTreeQuery
|
||||
//
|
||||
// @Summary Get Locations Tree
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @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]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLocationTreeQuery() server.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
user := services.UseUserCtx(r.Context())
|
||||
|
||||
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})
|
||||
func (ctrl *V1Controller) HandleLocationTreeQuery() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request, query repo.TreeQuery) ([]repo.TreeItem, error) {
|
||||
auth := services.NewContext(r.Context())
|
||||
return ctrl.repo.Locations.Tree(auth, auth.GID, query)
|
||||
}
|
||||
|
||||
return adapters.Query(fn, http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleLocationGetAll godoc
|
||||
// HandleLocationGetAll
|
||||
//
|
||||
// @Summary Get All Locations
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @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]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLocationGetAll() server.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
user := services.UseUserCtx(r.Context())
|
||||
|
||||
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})
|
||||
func (ctrl *V1Controller) HandleLocationGetAll() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request, q repo.LocationQuery) ([]repo.LocationOutCount, error) {
|
||||
auth := services.NewContext(r.Context())
|
||||
return ctrl.repo.Locations.GetAll(auth, auth.GID, q)
|
||||
}
|
||||
|
||||
return adapters.Query(fn, http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleLocationCreate godoc
|
||||
// HandleLocationCreate
|
||||
//
|
||||
// @Summary Create Location
|
||||
// @Tags Locations
|
||||
|
@ -82,26 +55,16 @@ func (ctrl *V1Controller) HandleLocationGetAll() server.HandlerFunc {
|
|||
// @Success 200 {object} repo.LocationSummary
|
||||
// @Router /v1/locations [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLocationCreate() server.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
createData := repo.LocationCreate{}
|
||||
if err := server.Decode(r, &createData); err != nil {
|
||||
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)
|
||||
func (ctrl *V1Controller) HandleLocationCreate() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request, createData repo.LocationCreate) (repo.LocationOut, error) {
|
||||
auth := services.NewContext(r.Context())
|
||||
return ctrl.repo.Locations.Create(auth, auth.GID, createData)
|
||||
}
|
||||
|
||||
return adapters.Action(fn, http.StatusCreated)
|
||||
}
|
||||
|
||||
// HandleLocationDelete godocs
|
||||
// HandleLocationDelete
|
||||
//
|
||||
// @Summary Delete Location
|
||||
// @Tags Locations
|
||||
|
@ -110,11 +73,17 @@ func (ctrl *V1Controller) HandleLocationCreate() server.HandlerFunc {
|
|||
// @Success 204
|
||||
// @Router /v1/locations/{id} [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLocationDelete() server.HandlerFunc {
|
||||
return ctrl.handleLocationGeneral()
|
||||
func (ctrl *V1Controller) HandleLocationDelete() errchain.HandlerFunc {
|
||||
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
|
||||
// @Tags Locations
|
||||
|
@ -123,11 +92,16 @@ func (ctrl *V1Controller) HandleLocationDelete() server.HandlerFunc {
|
|||
// @Success 200 {object} repo.LocationOut
|
||||
// @Router /v1/locations/{id} [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLocationGet() server.HandlerFunc {
|
||||
return ctrl.handleLocationGeneral()
|
||||
func (ctrl *V1Controller) HandleLocationGet() errchain.HandlerFunc {
|
||||
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
|
||||
// @Tags Locations
|
||||
|
@ -137,58 +111,12 @@ func (ctrl *V1Controller) HandleLocationGet() server.HandlerFunc {
|
|||
// @Success 200 {object} repo.LocationOut
|
||||
// @Router /v1/locations/{id} [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLocationUpdate() server.HandlerFunc {
|
||||
return ctrl.handleLocationGeneral()
|
||||
}
|
||||
|
||||
func (ctrl *V1Controller) handleLocationGeneral() server.HandlerFunc {
|
||||
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
|
||||
func (ctrl *V1Controller) HandleLocationUpdate() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request, ID uuid.UUID, body repo.LocationUpdate) (repo.LocationOut, error) {
|
||||
auth := services.NewContext(r.Context())
|
||||
body.ID = ID
|
||||
return ctrl.repo.Locations.UpdateByGroup(auth, auth.GID, ID, body)
|
||||
}
|
||||
|
||||
return adapters.ActionID("id", fn, http.StatusOK)
|
||||
}
|
||||
|
|
|
@ -2,13 +2,12 @@ package v1
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"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/pkgs/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/hay-kot/homebox/backend/internal/web/adapters"
|
||||
"github.com/hay-kot/safeserve/errchain"
|
||||
)
|
||||
|
||||
// HandleMaintenanceGetLog godoc
|
||||
|
@ -19,8 +18,13 @@ import (
|
|||
// @Success 200 {object} repo.MaintenanceLog
|
||||
// @Router /v1/items/{id}/maintenance [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleMaintenanceLogGet() server.HandlerFunc {
|
||||
return ctrl.handleMaintenanceLog()
|
||||
func (ctrl *V1Controller) HandleMaintenanceLogGet() errchain.HandlerFunc {
|
||||
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
|
||||
|
@ -29,11 +33,16 @@ func (ctrl *V1Controller) HandleMaintenanceLogGet() server.HandlerFunc {
|
|||
// @Tags Maintenance
|
||||
// @Produce json
|
||||
// @Param payload body repo.MaintenanceEntryCreate true "Entry Data"
|
||||
// @Success 200 {object} repo.MaintenanceEntry
|
||||
// @Success 201 {object} repo.MaintenanceEntry
|
||||
// @Router /v1/items/{id}/maintenance [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleMaintenanceEntryCreate() server.HandlerFunc {
|
||||
return ctrl.handleMaintenanceLog()
|
||||
func (ctrl *V1Controller) HandleMaintenanceEntryCreate() errchain.HandlerFunc {
|
||||
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
|
||||
|
@ -44,8 +53,14 @@ func (ctrl *V1Controller) HandleMaintenanceEntryCreate() server.HandlerFunc {
|
|||
// @Success 204
|
||||
// @Router /v1/items/{id}/maintenance/{entry_id} [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleMaintenanceEntryDelete() server.HandlerFunc {
|
||||
return ctrl.handleMaintenanceLog()
|
||||
func (ctrl *V1Controller) HandleMaintenanceEntryDelete() errchain.HandlerFunc {
|
||||
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
|
||||
|
@ -57,81 +72,11 @@ func (ctrl *V1Controller) HandleMaintenanceEntryDelete() server.HandlerFunc {
|
|||
// @Success 200 {object} repo.MaintenanceEntry
|
||||
// @Router /v1/items/{id}/maintenance/{entry_id} [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleMaintenanceEntryUpdate() server.HandlerFunc {
|
||||
return ctrl.handleMaintenanceLog()
|
||||
}
|
||||
|
||||
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
|
||||
func (ctrl *V1Controller) HandleMaintenanceEntryUpdate() errchain.HandlerFunc {
|
||||
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)
|
||||
}
|
||||
|
||||
return adapters.ActionID("entry_id", fn, http.StatusOK)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"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/data/repo"
|
||||
"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
|
||||
|
@ -17,13 +16,13 @@ import (
|
|||
// @Summary Get Notifiers
|
||||
// @Tags Notifiers
|
||||
// @Produce json
|
||||
// @Success 200 {object} server.Results{items=[]repo.NotifierOut}
|
||||
// @Success 200 {object} Wrapped{items=[]repo.NotifierOut}
|
||||
// @Router /v1/notifiers [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleGetUserNotifiers() server.HandlerFunc {
|
||||
fn := func(ctx context.Context, _ struct{}) ([]repo.NotifierOut, error) {
|
||||
user := services.UseUserCtx(ctx)
|
||||
return ctrl.repo.Notifiers.GetByUser(ctx, user.ID)
|
||||
func (ctrl *V1Controller) HandleGetUserNotifiers() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request, _ struct{}) ([]repo.NotifierOut, error) {
|
||||
user := services.UseUserCtx(r.Context())
|
||||
return ctrl.repo.Notifiers.GetByUser(r.Context(), user.ID)
|
||||
}
|
||||
|
||||
return adapters.Query(fn, http.StatusOK)
|
||||
|
@ -38,10 +37,10 @@ func (ctrl *V1Controller) HandleGetUserNotifiers() server.HandlerFunc {
|
|||
// @Success 200 {object} repo.NotifierOut
|
||||
// @Router /v1/notifiers [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleCreateNotifier() server.HandlerFunc {
|
||||
fn := func(ctx context.Context, in repo.NotifierCreate) (repo.NotifierOut, error) {
|
||||
auth := services.NewContext(ctx)
|
||||
return ctrl.repo.Notifiers.Create(ctx, auth.GID, auth.UID, in)
|
||||
func (ctrl *V1Controller) HandleCreateNotifier() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request, in repo.NotifierCreate) (repo.NotifierOut, error) {
|
||||
auth := services.NewContext(r.Context())
|
||||
return ctrl.repo.Notifiers.Create(auth, auth.GID, auth.UID, in)
|
||||
}
|
||||
|
||||
return adapters.Action(fn, http.StatusCreated)
|
||||
|
@ -55,10 +54,10 @@ func (ctrl *V1Controller) HandleCreateNotifier() server.HandlerFunc {
|
|||
// @Success 204
|
||||
// @Router /v1/notifiers/{id} [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleDeleteNotifier() server.HandlerFunc {
|
||||
fn := func(ctx context.Context, ID uuid.UUID) (any, error) {
|
||||
auth := services.NewContext(ctx)
|
||||
return nil, ctrl.repo.Notifiers.Delete(ctx, auth.UID, ID)
|
||||
func (ctrl *V1Controller) HandleDeleteNotifier() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
|
||||
auth := services.NewContext(r.Context())
|
||||
return nil, ctrl.repo.Notifiers.Delete(auth, auth.UID, ID)
|
||||
}
|
||||
|
||||
return adapters.CommandID("id", fn, http.StatusNoContent)
|
||||
|
@ -73,10 +72,10 @@ func (ctrl *V1Controller) HandleDeleteNotifier() server.HandlerFunc {
|
|||
// @Success 200 {object} repo.NotifierOut
|
||||
// @Router /v1/notifiers/{id} [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleUpdateNotifier() server.HandlerFunc {
|
||||
fn := func(ctx context.Context, ID uuid.UUID, in repo.NotifierUpdate) (repo.NotifierOut, error) {
|
||||
auth := services.NewContext(ctx)
|
||||
return ctrl.repo.Notifiers.Update(ctx, auth.UID, ID, in)
|
||||
func (ctrl *V1Controller) HandleUpdateNotifier() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request, ID uuid.UUID, in repo.NotifierUpdate) (repo.NotifierOut, error) {
|
||||
auth := services.NewContext(r.Context())
|
||||
return ctrl.repo.Notifiers.Update(auth, auth.UID, ID, in)
|
||||
}
|
||||
|
||||
return adapters.ActionID("id", fn, http.StatusOK)
|
||||
|
@ -92,12 +91,12 @@ func (ctrl *V1Controller) HandleUpdateNotifier() server.HandlerFunc {
|
|||
// @Success 204
|
||||
// @Router /v1/notifiers/test [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandlerNotifierTest() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandlerNotifierTest() errchain.HandlerFunc {
|
||||
type body struct {
|
||||
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")
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
|
||||
"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/yeqown/go-qrcode/v2"
|
||||
"github.com/yeqown/go-qrcode/writer/standard"
|
||||
|
||||
|
@ -26,25 +26,24 @@ var qrcodeLogo []byte
|
|||
// @Success 200 {string} string "image/jpeg"
|
||||
// @Router /v1/qrcode [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleGenerateQRCode() server.HandlerFunc {
|
||||
const MaxLength = 4_296 // assume alphanumeric characters only
|
||||
func (ctrl *V1Controller) HandleGenerateQRCode() errchain.HandlerFunc {
|
||||
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 {
|
||||
data := r.URL.Query().Get("data")
|
||||
q, err := adapters.DecodeQuery[query](r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
image, err := png.Decode(bytes.NewReader(qrcodeLogo))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if len(data) > MaxLength {
|
||||
return validate.NewFieldErrors(validate.FieldError{
|
||||
Field: "data",
|
||||
Error: "max length is 4,296 characters exceeded",
|
||||
})
|
||||
}
|
||||
|
||||
qrc, err := qrcode.New(data)
|
||||
qrc, err := qrcode.New(q.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"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
|
||||
|
@ -15,7 +15,7 @@ import (
|
|||
// @Success 200 {string} string "text/csv"
|
||||
// @Router /v1/reporting/bill-of-materials [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleBillOfMaterialsExport() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleBillOfMaterialsExport() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
actor := services.UseUserCtx(r.Context())
|
||||
|
||||
|
|
|
@ -5,8 +5,11 @@ import (
|
|||
"time"
|
||||
|
||||
"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/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
|
||||
|
@ -17,17 +20,13 @@ import (
|
|||
// @Success 200 {object} []repo.TotalsByOrganizer
|
||||
// @Router /v1/groups/statistics/locations [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleGroupStatisticsLocations() server.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx := services.NewContext(r.Context())
|
||||
|
||||
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)
|
||||
func (ctrl *V1Controller) HandleGroupStatisticsLocations() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request) ([]repo.TotalsByOrganizer, error) {
|
||||
auth := services.NewContext(r.Context())
|
||||
return ctrl.repo.Groups.StatsLocationsByPurchasePrice(auth, auth.GID)
|
||||
}
|
||||
|
||||
return adapters.Command(fn, http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleGroupStatisticsLabels godoc
|
||||
|
@ -38,17 +37,13 @@ func (ctrl *V1Controller) HandleGroupStatisticsLocations() server.HandlerFunc {
|
|||
// @Success 200 {object} []repo.TotalsByOrganizer
|
||||
// @Router /v1/groups/statistics/labels [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleGroupStatisticsLabels() server.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx := services.NewContext(r.Context())
|
||||
|
||||
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)
|
||||
func (ctrl *V1Controller) HandleGroupStatisticsLabels() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request) ([]repo.TotalsByOrganizer, error) {
|
||||
auth := services.NewContext(r.Context())
|
||||
return ctrl.repo.Groups.StatsLabelsByPurchasePrice(auth, auth.GID)
|
||||
}
|
||||
|
||||
return adapters.Command(fn, http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleGroupStatistics godoc
|
||||
|
@ -59,17 +54,13 @@ func (ctrl *V1Controller) HandleGroupStatisticsLabels() server.HandlerFunc {
|
|||
// @Success 200 {object} repo.GroupStatistics
|
||||
// @Router /v1/groups/statistics [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleGroupStatistics() server.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx := services.NewContext(r.Context())
|
||||
|
||||
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)
|
||||
func (ctrl *V1Controller) HandleGroupStatistics() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request) (repo.GroupStatistics, error) {
|
||||
auth := services.NewContext(r.Context())
|
||||
return ctrl.repo.Groups.StatsGroup(auth, auth.GID)
|
||||
}
|
||||
|
||||
return adapters.Command(fn, http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleGroupStatisticsPriceOverTime godoc
|
||||
|
@ -82,7 +73,7 @@ func (ctrl *V1Controller) HandleGroupStatistics() server.HandlerFunc {
|
|||
// @Param end query string false "end date"
|
||||
// @Router /v1/groups/statistics/purchase-price [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleGroupStatisticsPriceOverTime() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleGroupStatisticsPriceOverTime() errchain.HandlerFunc {
|
||||
parseDate := func(datestr string, defaultDate time.Time) (time.Time, error) {
|
||||
if datestr == "" {
|
||||
return defaultDate, nil
|
||||
|
@ -108,6 +99,6 @@ func (ctrl *V1Controller) HandleGroupStatisticsPriceOverTime() server.HandlerFun
|
|||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return server.Respond(w, http.StatusOK, stats)
|
||||
return server.JSON(w, http.StatusOK, stats)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,8 @@ import (
|
|||
"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/pkgs/server"
|
||||
"github.com/hay-kot/safeserve/errchain"
|
||||
"github.com/hay-kot/safeserve/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
|
@ -20,7 +21,7 @@ import (
|
|||
// @Param payload body services.UserRegistration true "User Data"
|
||||
// @Success 204
|
||||
// @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 {
|
||||
regData := services.UserRegistration{}
|
||||
|
||||
|
@ -39,7 +40,7 @@ func (ctrl *V1Controller) HandleUserRegistration() server.HandlerFunc {
|
|||
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
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Success 200 {object} server.Result{item=repo.UserOut}
|
||||
// @Success 200 {object} Wrapped{item=repo.UserOut}
|
||||
// @Router /v1/users/self [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleUserSelf() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleUserSelf() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
token := services.UseTokenCtx(r.Context())
|
||||
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 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
|
||||
// @Produce json
|
||||
// @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]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleUserSelfUpdate() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleUserSelfUpdate() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
updateData := repo.UserUpdate{}
|
||||
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 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
|
||||
// @Router /v1/users/self [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleUserSelfDelete() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleUserSelfDelete() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
if ctrl.isDemo {
|
||||
return validate.NewRequestError(nil, http.StatusForbidden)
|
||||
|
@ -110,7 +111,7 @@ func (ctrl *V1Controller) HandleUserSelfDelete() server.HandlerFunc {
|
|||
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"
|
||||
// @Router /v1/users/change-password [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleUserSelfChangePassword() server.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleUserSelfChangePassword() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
if ctrl.isDemo {
|
||||
return validate.NewRequestError(nil, http.StatusForbidden)
|
||||
|
@ -148,6 +149,6 @@ func (ctrl *V1Controller) HandleUserSelfChangePassword() server.HandlerFunc {
|
|||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return server.Respond(w, http.StatusNoContent, nil)
|
||||
return server.JSON(w, http.StatusNoContent, nil)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,9 @@ import (
|
|||
|
||||
atlas "ariga.io/atlas/sql/migrate"
|
||||
"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/internal/core/services"
|
||||
"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/sys/config"
|
||||
"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/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -38,6 +44,8 @@ var (
|
|||
// @name Authorization
|
||||
// @description "Type 'Bearer TOKEN' to correctly set the API Key"
|
||||
func main() {
|
||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||
|
||||
cfg, err := config.New()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -118,26 +126,27 @@ func run(cfg *config.Config) error {
|
|||
)
|
||||
|
||||
// =========================================================================
|
||||
// Start Server\
|
||||
// Start Server
|
||||
|
||||
logger := log.With().Caller().Logger()
|
||||
|
||||
mwLogger := mid.Logger(logger)
|
||||
if app.conf.Mode == config.ModeDevelopment {
|
||||
mwLogger = mid.SugarLogger(logger)
|
||||
}
|
||||
router := chi.NewMux()
|
||||
router.Use(
|
||||
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(
|
||||
server.WithHost(app.conf.Web.Host),
|
||||
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)
|
||||
|
||||
// =========================================================================
|
||||
|
@ -175,5 +184,5 @@ func run(cfg *config.Config) error {
|
|||
}()
|
||||
}
|
||||
|
||||
return app.server.Start()
|
||||
return app.server.Start(router)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
|
||||
"github.com/hay-kot/homebox/backend/internal/core/services"
|
||||
"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 {
|
||||
|
@ -30,9 +30,9 @@ const (
|
|||
// the required roles, a 403 Forbidden will be returned.
|
||||
//
|
||||
// WARNING: This middleware _MUST_ be called after mwAuthToken or else it will panic
|
||||
func (a *app) mwRoles(rm RoleMode, required ...string) server.Middleware {
|
||||
return func(next server.Handler) server.Handler {
|
||||
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
func (a *app) mwRoles(rm RoleMode, required ...string) errchain.Middleware {
|
||||
return func(next errchain.Handler) errchain.Handler {
|
||||
return errchain.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx := r.Context()
|
||||
|
||||
maybeToken := ctx.Value(hashedToken)
|
||||
|
@ -116,8 +116,8 @@ func getCookie(r *http.Request) (string, error) {
|
|||
// - header = "Bearer 1234567890"
|
||||
// - query = "?access_token=1234567890"
|
||||
// - cookie = hb.auth.token = 1234567890
|
||||
func (a *app) mwAuthToken(next server.Handler) server.Handler {
|
||||
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
func (a *app) mwAuthToken(next errchain.Handler) errchain.Handler {
|
||||
return errchain.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
keyFuncs := [...]KeyFunc{
|
||||
getBearer,
|
||||
getCookie,
|
||||
|
|
|
@ -10,12 +10,13 @@ import (
|
|||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/hay-kot/homebox/backend/app/api/handlers/debughandlers"
|
||||
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/internal/data/ent/authroles"
|
||||
"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
|
||||
)
|
||||
|
||||
|
@ -36,12 +37,12 @@ func (a *app) debugRouter() *http.ServeMux {
|
|||
}
|
||||
|
||||
// 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()
|
||||
|
||||
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)),
|
||||
)))
|
||||
))
|
||||
|
||||
// =========================================================================
|
||||
// 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
|
||||
)
|
||||
|
||||
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,
|
||||
Commit: commit,
|
||||
BuildTime: buildTime,
|
||||
}))
|
||||
})))
|
||||
|
||||
a.server.Post(v1Base("/users/register"), v1Ctrl.HandleUserRegistration())
|
||||
a.server.Post(v1Base("/users/login"), v1Ctrl.HandleAuthLogin())
|
||||
r.Post(v1Base("/users/register"), chain.ToHandlerFunc(v1Ctrl.HandleUserRegistration()))
|
||||
r.Post(v1Base("/users/login"), chain.ToHandlerFunc(v1Ctrl.HandleAuthLogin()))
|
||||
|
||||
userMW := []server.Middleware{
|
||||
userMW := []errchain.Middleware{
|
||||
a.mwAuthToken,
|
||||
a.mwRoles(RoleModeOr, authroles.RoleUser.String()),
|
||||
}
|
||||
|
||||
a.server.Get(v1Base("/users/self"), v1Ctrl.HandleUserSelf(), userMW...)
|
||||
a.server.Put(v1Base("/users/self"), v1Ctrl.HandleUserSelfUpdate(), userMW...)
|
||||
a.server.Delete(v1Base("/users/self"), v1Ctrl.HandleUserSelfDelete(), userMW...)
|
||||
a.server.Post(v1Base("/users/logout"), v1Ctrl.HandleAuthLogout(), userMW...)
|
||||
a.server.Get(v1Base("/users/refresh"), v1Ctrl.HandleAuthRefresh(), userMW...)
|
||||
a.server.Put(v1Base("/users/self/change-password"), v1Ctrl.HandleUserSelfChangePassword(), userMW...)
|
||||
r.Get(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelf(), userMW...))
|
||||
r.Put(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfUpdate(), userMW...))
|
||||
r.Delete(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfDelete(), userMW...))
|
||||
r.Post(v1Base("/users/logout"), chain.ToHandlerFunc(v1Ctrl.HandleAuthLogout(), userMW...))
|
||||
r.Get(v1Base("/users/refresh"), chain.ToHandlerFunc(v1Ctrl.HandleAuthRefresh(), userMW...))
|
||||
r.Put(v1Base("/users/self/change-password"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfChangePassword(), userMW...))
|
||||
|
||||
a.server.Post(v1Base("/groups/invitations"), v1Ctrl.HandleGroupInvitationsCreate(), userMW...)
|
||||
a.server.Get(v1Base("/groups/statistics"), v1Ctrl.HandleGroupStatistics(), userMW...)
|
||||
a.server.Get(v1Base("/groups/statistics/purchase-price"), v1Ctrl.HandleGroupStatisticsPriceOverTime(), userMW...)
|
||||
a.server.Get(v1Base("/groups/statistics/locations"), v1Ctrl.HandleGroupStatisticsLocations(), userMW...)
|
||||
a.server.Get(v1Base("/groups/statistics/labels"), v1Ctrl.HandleGroupStatisticsLabels(), userMW...)
|
||||
r.Post(v1Base("/groups/invitations"), chain.ToHandlerFunc(v1Ctrl.HandleGroupInvitationsCreate(), userMW...))
|
||||
r.Get(v1Base("/groups/statistics"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatistics(), userMW...))
|
||||
r.Get(v1Base("/groups/statistics/purchase-price"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsPriceOverTime(), userMW...))
|
||||
r.Get(v1Base("/groups/statistics/locations"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLocations(), userMW...))
|
||||
r.Get(v1Base("/groups/statistics/labels"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLabels(), userMW...))
|
||||
|
||||
// TODO: I don't like /groups being the URL for users
|
||||
a.server.Get(v1Base("/groups"), v1Ctrl.HandleGroupGet(), userMW...)
|
||||
a.server.Put(v1Base("/groups"), v1Ctrl.HandleGroupUpdate(), userMW...)
|
||||
r.Get(v1Base("/groups"), chain.ToHandlerFunc(v1Ctrl.HandleGroupGet(), userMW...))
|
||||
r.Put(v1Base("/groups"), chain.ToHandlerFunc(v1Ctrl.HandleGroupUpdate(), userMW...))
|
||||
|
||||
a.server.Post(v1Base("/actions/ensure-asset-ids"), v1Ctrl.HandleEnsureAssetID(), userMW...)
|
||||
a.server.Post(v1Base("/actions/zero-item-time-fields"), v1Ctrl.HandleItemDateZeroOut(), userMW...)
|
||||
a.server.Post(v1Base("/actions/ensure-import-refs"), v1Ctrl.HandleEnsureImportRefs(), userMW...)
|
||||
r.Post(v1Base("/actions/ensure-asset-ids"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureAssetID(), userMW...))
|
||||
r.Post(v1Base("/actions/zero-item-time-fields"), chain.ToHandlerFunc(v1Ctrl.HandleItemDateZeroOut(), userMW...))
|
||||
r.Post(v1Base("/actions/ensure-import-refs"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
|
||||
|
||||
a.server.Get(v1Base("/locations"), v1Ctrl.HandleLocationGetAll(), userMW...)
|
||||
a.server.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate(), userMW...)
|
||||
a.server.Get(v1Base("/locations/tree"), v1Ctrl.HandleLocationTreeQuery(), userMW...)
|
||||
a.server.Get(v1Base("/locations/{id}"), v1Ctrl.HandleLocationGet(), userMW...)
|
||||
a.server.Put(v1Base("/locations/{id}"), v1Ctrl.HandleLocationUpdate(), userMW...)
|
||||
a.server.Delete(v1Base("/locations/{id}"), v1Ctrl.HandleLocationDelete(), userMW...)
|
||||
r.Get(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
|
||||
r.Post(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))
|
||||
r.Get(v1Base("/locations/tree"), chain.ToHandlerFunc(v1Ctrl.HandleLocationTreeQuery(), userMW...))
|
||||
r.Get(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationGet(), userMW...))
|
||||
r.Put(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationUpdate(), userMW...))
|
||||
r.Delete(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationDelete(), userMW...))
|
||||
|
||||
a.server.Get(v1Base("/labels"), v1Ctrl.HandleLabelsGetAll(), userMW...)
|
||||
a.server.Post(v1Base("/labels"), v1Ctrl.HandleLabelsCreate(), userMW...)
|
||||
a.server.Get(v1Base("/labels/{id}"), v1Ctrl.HandleLabelGet(), userMW...)
|
||||
a.server.Put(v1Base("/labels/{id}"), v1Ctrl.HandleLabelUpdate(), userMW...)
|
||||
a.server.Delete(v1Base("/labels/{id}"), v1Ctrl.HandleLabelDelete(), userMW...)
|
||||
r.Get(v1Base("/labels"), chain.ToHandlerFunc(v1Ctrl.HandleLabelsGetAll(), userMW...))
|
||||
r.Post(v1Base("/labels"), chain.ToHandlerFunc(v1Ctrl.HandleLabelsCreate(), userMW...))
|
||||
r.Get(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelGet(), userMW...))
|
||||
r.Put(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelUpdate(), userMW...))
|
||||
r.Delete(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelDelete(), userMW...))
|
||||
|
||||
a.server.Get(v1Base("/items"), v1Ctrl.HandleItemsGetAll(), userMW...)
|
||||
a.server.Post(v1Base("/items"), v1Ctrl.HandleItemsCreate(), userMW...)
|
||||
a.server.Post(v1Base("/items/import"), v1Ctrl.HandleItemsImport(), userMW...)
|
||||
a.server.Get(v1Base("/items/export"), v1Ctrl.HandleItemsExport(), userMW...)
|
||||
a.server.Get(v1Base("/items/fields"), v1Ctrl.HandleGetAllCustomFieldNames(), userMW...)
|
||||
a.server.Get(v1Base("/items/fields/values"), v1Ctrl.HandleGetAllCustomFieldValues(), userMW...)
|
||||
r.Get(v1Base("/items"), chain.ToHandlerFunc(v1Ctrl.HandleItemsGetAll(), userMW...))
|
||||
r.Post(v1Base("/items"), chain.ToHandlerFunc(v1Ctrl.HandleItemsCreate(), userMW...))
|
||||
r.Post(v1Base("/items/import"), chain.ToHandlerFunc(v1Ctrl.HandleItemsImport(), userMW...))
|
||||
r.Get(v1Base("/items/export"), chain.ToHandlerFunc(v1Ctrl.HandleItemsExport(), userMW...))
|
||||
r.Get(v1Base("/items/fields"), chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldNames(), userMW...))
|
||||
r.Get(v1Base("/items/fields/values"), chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldValues(), userMW...))
|
||||
|
||||
a.server.Get(v1Base("/items/{id}"), v1Ctrl.HandleItemGet(), userMW...)
|
||||
a.server.Put(v1Base("/items/{id}"), v1Ctrl.HandleItemUpdate(), userMW...)
|
||||
a.server.Delete(v1Base("/items/{id}"), v1Ctrl.HandleItemDelete(), userMW...)
|
||||
r.Get(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemGet(), userMW...))
|
||||
r.Put(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...))
|
||||
r.Delete(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...))
|
||||
|
||||
a.server.Post(v1Base("/items/{id}/attachments"), v1Ctrl.HandleItemAttachmentCreate(), userMW...)
|
||||
a.server.Put(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentUpdate(), userMW...)
|
||||
a.server.Delete(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentDelete(), userMW...)
|
||||
r.Post(v1Base("/items/{id}/attachments"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...))
|
||||
r.Put(v1Base("/items/{id}/attachments/{attachment_id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...))
|
||||
r.Delete(v1Base("/items/{id}/attachments/{attachment_id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentDelete(), userMW...))
|
||||
|
||||
a.server.Get(v1Base("/items/{id}/maintenance"), v1Ctrl.HandleMaintenanceEntryCreate(), userMW...)
|
||||
a.server.Post(v1Base("/items/{id}/maintenance"), v1Ctrl.HandleMaintenanceEntryCreate(), userMW...)
|
||||
a.server.Put(v1Base("/items/{id}/maintenance/{entry_id}"), v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...)
|
||||
a.server.Delete(v1Base("/items/{id}/maintenance/{entry_id}"), v1Ctrl.HandleMaintenanceEntryDelete(), userMW...)
|
||||
r.Get(v1Base("/items/{id}/maintenance"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceLogGet(), userMW...))
|
||||
r.Post(v1Base("/items/{id}/maintenance"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryCreate(), userMW...))
|
||||
r.Put(v1Base("/items/{id}/maintenance/{entry_id}"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryUpdate(), 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
|
||||
a.server.Get(v1Base("/notifiers"), v1Ctrl.HandleGetUserNotifiers(), userMW...)
|
||||
a.server.Post(v1Base("/notifiers"), v1Ctrl.HandleCreateNotifier(), userMW...)
|
||||
a.server.Put(v1Base("/notifiers/{id}"), v1Ctrl.HandleUpdateNotifier(), userMW...)
|
||||
a.server.Delete(v1Base("/notifiers/{id}"), v1Ctrl.HandleDeleteNotifier(), userMW...)
|
||||
a.server.Post(v1Base("/notifiers/test"), v1Ctrl.HandlerNotifierTest(), userMW...)
|
||||
r.Get(v1Base("/notifiers"), chain.ToHandlerFunc(v1Ctrl.HandleGetUserNotifiers(), userMW...))
|
||||
r.Post(v1Base("/notifiers"), chain.ToHandlerFunc(v1Ctrl.HandleCreateNotifier(), userMW...))
|
||||
r.Put(v1Base("/notifiers/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleUpdateNotifier(), userMW...))
|
||||
r.Delete(v1Base("/notifiers/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleDeleteNotifier(), userMW...))
|
||||
r.Post(v1Base("/notifiers/test"), chain.ToHandlerFunc(v1Ctrl.HandlerNotifierTest(), userMW...))
|
||||
|
||||
// Asset-Like endpoints
|
||||
a.server.Get(
|
||||
assetMW := []errchain.Middleware{
|
||||
a.mwAuthToken,
|
||||
a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
|
||||
}
|
||||
|
||||
r.Get(
|
||||
v1Base("/qrcode"),
|
||||
v1Ctrl.HandleGenerateQRCode(),
|
||||
a.mwAuthToken, a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
|
||||
chain.ToHandlerFunc(v1Ctrl.HandleGenerateQRCode(), assetMW...),
|
||||
)
|
||||
a.server.Get(
|
||||
r.Get(
|
||||
v1Base("/items/{id}/attachments/{attachment_id}"),
|
||||
v1Ctrl.HandleItemAttachmentGet(),
|
||||
a.mwAuthToken, a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
|
||||
chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentGet(), assetMW...),
|
||||
)
|
||||
|
||||
// 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() {
|
||||
|
@ -165,7 +170,7 @@ func registerMimes() {
|
|||
|
||||
// notFoundHandler perform the main logic around handling the internal SPA embed and ensuring that
|
||||
// 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 {
|
||||
f, err := fs.Open(path.Join(prefix, requestedPath))
|
||||
if err != nil {
|
||||
|
|
|
@ -425,8 +425,8 @@ const docTemplate = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.ItemSummary"
|
||||
}
|
||||
|
@ -694,7 +694,7 @@ const docTemplate = `{
|
|||
"422": {
|
||||
"description": "Unprocessable Entity",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
"$ref": "#/definitions/mid.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -864,8 +864,8 @@ const docTemplate = `{
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.MaintenanceEntry"
|
||||
}
|
||||
|
@ -947,7 +947,7 @@ const docTemplate = `{
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Results"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1119,7 +1119,7 @@ const docTemplate = `{
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Results"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1199,7 +1199,7 @@ const docTemplate = `{
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Results"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1339,7 +1339,7 @@ const docTemplate = `{
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Results"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1719,7 +1719,7 @@ const docTemplate = `{
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1764,7 +1764,7 @@ const docTemplate = `{
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1801,6 +1801,20 @@ const docTemplate = `{
|
|||
}
|
||||
},
|
||||
"definitions": {
|
||||
"mid.ErrorResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"fields": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.DocumentOut": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -1902,9 +1916,15 @@ const docTemplate = `{
|
|||
},
|
||||
"repo.ItemCreate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 1000,
|
||||
"minLength": 1
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
|
@ -1917,7 +1937,9 @@ const docTemplate = `{
|
|||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 1
|
||||
},
|
||||
"parentId": {
|
||||
"type": "string",
|
||||
|
@ -2208,15 +2230,21 @@ const docTemplate = `{
|
|||
},
|
||||
"repo.LabelCreate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -2791,12 +2786,17 @@ const docTemplate = `{
|
|||
},
|
||||
"v1.GroupInvitationCreate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"uses"
|
||||
],
|
||||
"properties": {
|
||||
"expiresAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"uses": {
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"maximum": 100,
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -2821,6 +2821,12 @@ const docTemplate = `{
|
|||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.Wrapped": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
|
|
|
@ -417,8 +417,8 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.ItemSummary"
|
||||
}
|
||||
|
@ -686,7 +686,7 @@
|
|||
"422": {
|
||||
"description": "Unprocessable Entity",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
"$ref": "#/definitions/mid.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -856,8 +856,8 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.MaintenanceEntry"
|
||||
}
|
||||
|
@ -939,7 +939,7 @@
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Results"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1111,7 +1111,7 @@
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Results"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1191,7 +1191,7 @@
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Results"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1331,7 +1331,7 @@
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Results"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1711,7 +1711,7 @@
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1756,7 +1756,7 @@
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1793,6 +1793,20 @@
|
|||
}
|
||||
},
|
||||
"definitions": {
|
||||
"mid.ErrorResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"fields": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.DocumentOut": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -1894,9 +1908,15 @@
|
|||
},
|
||||
"repo.ItemCreate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 1000,
|
||||
"minLength": 1
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
|
@ -1909,7 +1929,9 @@
|
|||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 1
|
||||
},
|
||||
"parentId": {
|
||||
"type": "string",
|
||||
|
@ -2200,15 +2222,21 @@
|
|||
},
|
||||
"repo.LabelCreate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -2783,12 +2778,17 @@
|
|||
},
|
||||
"v1.GroupInvitationCreate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"uses"
|
||||
],
|
||||
"properties": {
|
||||
"expiresAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"uses": {
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"maximum": 100,
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -2813,6 +2813,12 @@
|
|||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.Wrapped": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
basePath: /api
|
||||
definitions:
|
||||
mid.ErrorResponse:
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
fields:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
repo.DocumentOut:
|
||||
properties:
|
||||
id:
|
||||
|
@ -67,6 +76,8 @@ definitions:
|
|||
repo.ItemCreate:
|
||||
properties:
|
||||
description:
|
||||
maxLength: 1000
|
||||
minLength: 1
|
||||
type: string
|
||||
labelIds:
|
||||
items:
|
||||
|
@ -76,10 +87,15 @@ definitions:
|
|||
description: Edges
|
||||
type: string
|
||||
name:
|
||||
maxLength: 255
|
||||
minLength: 1
|
||||
type: string
|
||||
parentId:
|
||||
type: string
|
||||
x-nullable: true
|
||||
required:
|
||||
- description
|
||||
- name
|
||||
type: object
|
||||
repo.ItemField:
|
||||
properties:
|
||||
|
@ -281,9 +297,14 @@ definitions:
|
|||
color:
|
||||
type: string
|
||||
description:
|
||||
maxLength: 255
|
||||
type: string
|
||||
name:
|
||||
maxLength: 255
|
||||
minLength: 1
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
repo.LabelOut:
|
||||
properties:
|
||||
|
@ -579,28 +600,6 @@ definitions:
|
|||
value:
|
||||
type: number
|
||||
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:
|
||||
properties:
|
||||
email:
|
||||
|
@ -666,7 +665,11 @@ definitions:
|
|||
expiresAt:
|
||||
type: string
|
||||
uses:
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
type: integer
|
||||
required:
|
||||
- uses
|
||||
type: object
|
||||
v1.ItemAttachmentToken:
|
||||
properties:
|
||||
|
@ -682,6 +685,10 @@ definitions:
|
|||
token:
|
||||
type: string
|
||||
type: object
|
||||
v1.Wrapped:
|
||||
properties:
|
||||
item: {}
|
||||
type: object
|
||||
info:
|
||||
contact:
|
||||
name: Don't
|
||||
|
@ -932,8 +939,8 @@ paths:
|
|||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
"201":
|
||||
description: Created
|
||||
schema:
|
||||
$ref: '#/definitions/repo.ItemSummary'
|
||||
security:
|
||||
|
@ -1036,7 +1043,7 @@ paths:
|
|||
"422":
|
||||
description: Unprocessable Entity
|
||||
schema:
|
||||
$ref: '#/definitions/server.ErrorResponse'
|
||||
$ref: '#/definitions/mid.ErrorResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Create Item Attachment
|
||||
|
@ -1140,8 +1147,8 @@ paths:
|
|||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
"201":
|
||||
description: Created
|
||||
schema:
|
||||
$ref: '#/definitions/repo.MaintenanceEntry'
|
||||
security:
|
||||
|
@ -1252,7 +1259,7 @@ paths:
|
|||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/server.Results'
|
||||
- $ref: '#/definitions/v1.Wrapped'
|
||||
- properties:
|
||||
items:
|
||||
items:
|
||||
|
@ -1354,7 +1361,7 @@ paths:
|
|||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/server.Results'
|
||||
- $ref: '#/definitions/v1.Wrapped'
|
||||
- properties:
|
||||
items:
|
||||
items:
|
||||
|
@ -1462,7 +1469,7 @@ paths:
|
|||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/server.Results'
|
||||
- $ref: '#/definitions/v1.Wrapped'
|
||||
- properties:
|
||||
items:
|
||||
items:
|
||||
|
@ -1483,7 +1490,7 @@ paths:
|
|||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/server.Results'
|
||||
- $ref: '#/definitions/v1.Wrapped'
|
||||
- properties:
|
||||
items:
|
||||
items:
|
||||
|
@ -1725,7 +1732,7 @@ paths:
|
|||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/server.Result'
|
||||
- $ref: '#/definitions/v1.Wrapped'
|
||||
- properties:
|
||||
item:
|
||||
$ref: '#/definitions/repo.UserOut'
|
||||
|
@ -1750,7 +1757,7 @@ paths:
|
|||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/server.Result'
|
||||
- $ref: '#/definitions/v1.Wrapped'
|
||||
- properties:
|
||||
item:
|
||||
$ref: '#/definitions/repo.UserUpdate'
|
||||
|
|
|
@ -12,7 +12,9 @@ require (
|
|||
github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a
|
||||
github.com/google/uuid v1.3.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/pkg/errors v0.9.1
|
||||
github.com/rs/zerolog v1.29.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
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-isatty v0.0.17 // 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/swaggo/files v1.0.0 // indirect
|
||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||
|
|
|
@ -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/go.mod h1:+TR129FJZ5Lvzms6dvCeGWh1yR6hMvmXBhug4hrNIGk=
|
||||
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/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/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/go.mod h1:zclexWKe0NVj6LHQ8NgDDZ7bQ1spE0KeKPFficdtAjU=
|
||||
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/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/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/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
|
||||
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-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/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||
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/serf v0.9.7/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/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=
|
||||
|
@ -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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
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/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ=
|
||||
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.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
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/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
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.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
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/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.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.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
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/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/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/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
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/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
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/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/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4=
|
||||
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/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
|
||||
github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo=
|
||||
|
|
|
@ -51,8 +51,8 @@ type (
|
|||
ItemCreate struct {
|
||||
ImportRef string `json:"-"`
|
||||
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Description string `json:"description" validate:"required,min=1,max=1000"`
|
||||
AssetID AssetID `json:"-"`
|
||||
|
||||
// Edges
|
||||
|
|
|
@ -17,15 +17,15 @@ type LabelRepository struct {
|
|||
}
|
||||
type (
|
||||
LabelCreate struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Description string `json:"description" validate:"max=255"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
LabelUpdate struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Description string `json:"description" validate:"max=255"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ func mapLocationOut(location *ent.Location) LocationOut {
|
|||
}
|
||||
|
||||
type LocationQuery struct {
|
||||
FilterChildren bool `json:"filterChildren"`
|
||||
FilterChildren bool `json:"filterChildren" schema:"filterChildren"`
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
|
||||
|
@ -246,7 +246,7 @@ type FlatTreeItem 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) {
|
||||
|
|
|
@ -124,7 +124,7 @@ func TestItemRepository_TreeQuery(t *testing.T) {
|
|||
locs := useLocations(t, 3)
|
||||
|
||||
// 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,
|
||||
ParentID: locs[1].ID,
|
||||
Name: locs[0].Name,
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
|
||||
"github.com/google/uuid"
|
||||
"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/types"
|
||||
)
|
||||
|
@ -92,16 +94,21 @@ func (r *MaintenanceEntryRepository) Update(ctx context.Context, ID uuid.UUID, i
|
|||
}
|
||||
|
||||
type MaintenanceLogQuery struct {
|
||||
Completed bool
|
||||
Scheduled bool
|
||||
Completed bool `json:"completed" schema:"completed"`
|
||||
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{
|
||||
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 {
|
||||
q = q.Where(maintenanceentry.And(
|
||||
|
|
|
@ -59,7 +59,7 @@ func TestMaintenanceEntryRepository_GetLog(t *testing.T) {
|
|||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
@ -3,7 +3,8 @@ package adapters
|
|||
import (
|
||||
"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.
|
||||
|
@ -16,25 +17,25 @@ import (
|
|||
// 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
|
||||
// return nil, nil
|
||||
// }
|
||||
//
|
||||
// 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 {
|
||||
v, err := decode[T](r)
|
||||
v, err := DecodeBody[T](r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := f(r.Context(), v)
|
||||
res, err := f(r, v)
|
||||
if err != nil {
|
||||
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"`
|
||||
// }
|
||||
//
|
||||
// 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
|
||||
// return nil, nil
|
||||
// }
|
||||
//
|
||||
// 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 {
|
||||
ID, err := routeUUID(r, param)
|
||||
ID, err := RouteUUID(r, param)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := decode[T](r)
|
||||
v, err := DecodeBody[T](r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := f(r.Context(), ID, v)
|
||||
res, err := f(r, ID, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return server.Respond(w, ok, res)
|
||||
return server.JSON(w, ok, res)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AdapterFunc[T any, Y any] func(context.Context, T) (Y, error)
|
||||
type IDFunc[T any, Y any] func(context.Context, uuid.UUID, T) (Y, error)
|
||||
type AdapterFunc[T any, Y any] func(*http.Request, T) (Y, error)
|
||||
type IDFunc[T any, Y any] func(*http.Request, uuid.UUID, T) (Y, error)
|
||||
|
|
|
@ -1,36 +1,36 @@
|
|||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"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 CommandIDFunc[T any] func(context.Context, uuid.UUID) (T, error)
|
||||
type CommandFunc[T any] func(*http.Request) (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
|
||||
// or a query. You can think of them as a way to handle RPC style Rest Endpoints.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fn := func(ctx context.Context) (interface{}, error) {
|
||||
// // do something
|
||||
// return nil, nil
|
||||
// }
|
||||
// fn := func(r *http.Request) (interface{}, error) {
|
||||
// // do something
|
||||
// return nil, nil
|
||||
// }
|
||||
//
|
||||
// r.Get("/foo", adapters.Command(fn, http.NoContent))
|
||||
func Command[T any](f CommandFunc[T], ok int) server.HandlerFunc {
|
||||
// r.Get("/foo", adapters.Command(fn, http.NoContent))
|
||||
func Command[T any](f CommandFunc[T], ok int) errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
res, err := f(r.Context())
|
||||
res, err := f(r)
|
||||
if err != nil {
|
||||
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:
|
||||
//
|
||||
// fn := func(ctx context.Context, id uuid.UUID) (interface{}, error) {
|
||||
// fn := func(r *http.Request, id uuid.UUID) (interface{}, error) {
|
||||
// // do something
|
||||
// return nil, nil
|
||||
// }
|
||||
//
|
||||
// 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 {
|
||||
ID, err := routeUUID(r, param)
|
||||
ID, err := RouteUUID(r, param)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := f(r.Context(), ID)
|
||||
res, err := f(r, ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return server.Respond(w, ok, res)
|
||||
return server.JSON(w, ok, res)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,47 +3,49 @@ package adapters
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/schema"
|
||||
"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()
|
||||
|
||||
func decodeQuery[T any](r *http.Request) (T, error) {
|
||||
func DecodeQuery[T any](r *http.Request) (T, error) {
|
||||
var v T
|
||||
err := queryDecoder.Decode(&v, r.URL.Query())
|
||||
if err != nil {
|
||||
return v, err
|
||||
return v, errors.Wrap(err, "decoding error")
|
||||
}
|
||||
|
||||
err = validate.Check(v)
|
||||
if err != nil {
|
||||
return v, err
|
||||
return v, errors.Wrap(err, "validation error")
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decode[T any](r *http.Request) (T, error) {
|
||||
func DecodeBody[T any](r *http.Request) (T, error) {
|
||||
var v T
|
||||
|
||||
err := server.Decode(r, &v)
|
||||
if err != nil {
|
||||
return v, err
|
||||
return v, errors.Wrap(err, "body decoding error")
|
||||
}
|
||||
|
||||
err = validate.Check(v)
|
||||
if err != nil {
|
||||
return v, err
|
||||
return v, errors.Wrap(err, "validation error")
|
||||
}
|
||||
|
||||
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))
|
||||
if err != nil {
|
||||
return uuid.Nil, validate.NewRouteKeyError(key)
|
||||
|
|
|
@ -3,7 +3,8 @@ package adapters
|
|||
import (
|
||||
"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.
|
||||
|
@ -14,25 +15,25 @@ import (
|
|||
// 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
|
||||
// return nil, nil
|
||||
// }
|
||||
//
|
||||
// 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 {
|
||||
q, err := decodeQuery[T](r)
|
||||
q, err := DecodeQuery[T](r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := f(r.Context(), q)
|
||||
res, err := f(r, q)
|
||||
if err != nil {
|
||||
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"`
|
||||
// }
|
||||
//
|
||||
// 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
|
||||
// return nil, nil
|
||||
// }
|
||||
//
|
||||
// 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 {
|
||||
ID, err := routeUUID(r, param)
|
||||
ID, err := RouteUUID(r, param)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q, err := decodeQuery[T](r)
|
||||
q, err := DecodeQuery[T](r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := f(r.Context(), ID, q)
|
||||
res, err := f(r, ID, q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return server.Respond(w, ok, res)
|
||||
return server.JSON(w, ok, res)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,33 +3,42 @@ package mid
|
|||
import (
|
||||
"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/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"
|
||||
)
|
||||
|
||||
func Errors(log zerolog.Logger) server.Middleware {
|
||||
return func(h server.Handler) server.Handler {
|
||||
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"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)
|
||||
if err != nil {
|
||||
var resp server.ErrorResponse
|
||||
var resp ErrorResponse
|
||||
var code int
|
||||
|
||||
traceID := r.Context().Value(middleware.RequestIDKey).(string)
|
||||
log.Err(err).
|
||||
Str("trace_id", server.GetTraceID(r.Context())).
|
||||
Stack().
|
||||
Str("req_id", traceID).
|
||||
Msg("ERROR occurred")
|
||||
|
||||
switch {
|
||||
case validate.IsUnauthorizedError(err):
|
||||
code = http.StatusUnauthorized
|
||||
resp = server.ErrorResponse{
|
||||
resp = ErrorResponse{
|
||||
Error: "unauthorized",
|
||||
}
|
||||
case validate.IsInvalidRouteKeyError(err):
|
||||
code = http.StatusBadRequest
|
||||
resp = server.ErrorResponse{
|
||||
resp = ErrorResponse{
|
||||
Error: err.Error(),
|
||||
}
|
||||
case validate.IsFieldError(err):
|
||||
|
@ -59,17 +68,18 @@ func Errors(log zerolog.Logger) server.Middleware {
|
|||
code = http.StatusInternalServerError
|
||||
}
|
||||
|
||||
if err := server.Respond(w, code, resp); err != nil {
|
||||
return err
|
||||
if err := server.JSON(w, code, resp); err != nil {
|
||||
log.Err(err).Msg("failed to write response")
|
||||
}
|
||||
|
||||
// If Showdown error, return error
|
||||
if server.IsShutdownError(err) {
|
||||
return err
|
||||
err := svr.Shutdown(err.Error())
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to shutdown server")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,96 +1,33 @@
|
|||
package mid
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type statusRecorder struct {
|
||||
type spy struct {
|
||||
http.ResponseWriter
|
||||
Status int
|
||||
status int
|
||||
}
|
||||
|
||||
func (r *statusRecorder) WriteHeader(status int) {
|
||||
r.Status = status
|
||||
r.ResponseWriter.WriteHeader(status)
|
||||
func (s *spy) WriteHeader(status int) {
|
||||
s.status = status
|
||||
s.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
func Logger(log zerolog.Logger) server.Middleware {
|
||||
return func(next server.Handler) server.Handler {
|
||||
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
traceId := server.GetTraceID(r.Context())
|
||||
func Logger(l zerolog.Logger) func(http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
reqID := r.Context().Value(middleware.RequestIDKey).(string)
|
||||
|
||||
log.Info().
|
||||
Str("trace_id", traceId).
|
||||
Str("method", r.Method).
|
||||
Str("path", r.URL.Path).
|
||||
Str("remove_address", r.RemoteAddr).
|
||||
Msg("request started")
|
||||
l.Info().Str("method", r.Method).Str("path", r.URL.Path).Str("rid", reqID).Msg("request received")
|
||||
|
||||
record := &statusRecorder{ResponseWriter: w, Status: http.StatusOK}
|
||||
s := &spy{ResponseWriter: w}
|
||||
h.ServeHTTP(s, r)
|
||||
|
||||
err := next.ServeHTTP(record, r)
|
||||
|
||||
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
|
||||
l.Info().Str("method", r.Method).Str("path", r.URL.Path).Int("status", s.status).Str("rid", reqID).Msg("request finished")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
package server
|
||||
|
||||
const (
|
||||
ContentType = "Content-Type"
|
||||
ContentJSON = "application/json"
|
||||
ContentXML = "application/xml"
|
||||
ContentFormUrlEncoded = "application/x-www-form-urlencoded"
|
||||
)
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"))
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -417,8 +417,8 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.ItemSummary"
|
||||
}
|
||||
|
@ -686,7 +686,7 @@
|
|||
"422": {
|
||||
"description": "Unprocessable Entity",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ErrorResponse"
|
||||
"$ref": "#/definitions/mid.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -856,8 +856,8 @@
|
|||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.MaintenanceEntry"
|
||||
}
|
||||
|
@ -939,7 +939,7 @@
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Results"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1111,7 +1111,7 @@
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Results"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1191,7 +1191,7 @@
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Results"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1331,7 +1331,7 @@
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Results"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1711,7 +1711,7 @@
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1756,7 +1756,7 @@
|
|||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
"$ref": "#/definitions/v1.Wrapped"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -1793,6 +1793,20 @@
|
|||
}
|
||||
},
|
||||
"definitions": {
|
||||
"mid.ErrorResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"fields": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.DocumentOut": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -1894,9 +1908,15 @@
|
|||
},
|
||||
"repo.ItemCreate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 1000,
|
||||
"minLength": 1
|
||||
},
|
||||
"labelIds": {
|
||||
"type": "array",
|
||||
|
@ -1909,7 +1929,9 @@
|
|||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 1
|
||||
},
|
||||
"parentId": {
|
||||
"type": "string",
|
||||
|
@ -2200,15 +2222,21 @@
|
|||
},
|
||||
"repo.LabelCreate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -2783,12 +2778,17 @@
|
|||
},
|
||||
"v1.GroupInvitationCreate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"uses"
|
||||
],
|
||||
"properties": {
|
||||
"expiresAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"uses": {
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"maximum": 100,
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -2813,6 +2813,12 @@
|
|||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.Wrapped": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
|
|
|
@ -10,6 +10,11 @@
|
|||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export interface MidErrorResponse {
|
||||
error: string;
|
||||
fields: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface DocumentOut {
|
||||
id: string;
|
||||
path: string;
|
||||
|
@ -52,10 +57,18 @@ export interface ItemAttachmentUpdate {
|
|||
}
|
||||
|
||||
export interface ItemCreate {
|
||||
/**
|
||||
* @minLength 1
|
||||
* @maxLength 1000
|
||||
*/
|
||||
description: string;
|
||||
labelIds: string[];
|
||||
/** Edges */
|
||||
locationId: string;
|
||||
/**
|
||||
* @minLength 1
|
||||
* @maxLength 255
|
||||
*/
|
||||
name: string;
|
||||
parentId: string | null;
|
||||
}
|
||||
|
@ -164,7 +177,12 @@ export interface ItemUpdate {
|
|||
|
||||
export interface LabelCreate {
|
||||
color: string;
|
||||
/** @maxLength 255 */
|
||||
description: string;
|
||||
/**
|
||||
* @minLength 1
|
||||
* @maxLength 255
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
|
@ -346,22 +364,6 @@ export interface ValueOverTimeEntry {
|
|||
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 {
|
||||
email: string;
|
||||
name: string;
|
||||
|
@ -402,6 +404,10 @@ export interface GroupInvitation {
|
|||
|
||||
export interface GroupInvitationCreate {
|
||||
expiresAt: Date | string;
|
||||
/**
|
||||
* @min 1
|
||||
* @max 100
|
||||
*/
|
||||
uses: number;
|
||||
}
|
||||
|
||||
|
@ -414,3 +420,7 @@ export interface TokenResponse {
|
|||
expiresAt: Date | string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface Wrapped {
|
||||
item: any;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue