refactor: repositories (#28)

* cleanup unnecessary mocks

* refactor document storage location

* remove unused function

* move ownership to document types to repo package

* move types and mappers to repo package

* refactor sets to own package
This commit is contained in:
Hayden 2022-09-27 15:52:13 -08:00 committed by GitHub
parent 2e82398e5c
commit 343290a55a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
79 changed files with 3169 additions and 3160 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,302 @@
basePath: /api
definitions:
repo.DocumentOut:
properties:
id:
type: string
path:
type: string
title:
type: string
type: object
repo.ItemAttachment:
properties:
createdAt:
type: string
document:
$ref: '#/definitions/repo.DocumentOut'
id:
type: string
type:
type: string
updatedAt:
type: string
type: object
repo.ItemAttachmentUpdate:
properties:
title:
type: string
type:
type: string
type: object
repo.ItemCreate:
properties:
description:
type: string
labelIds:
items:
type: string
type: array
locationId:
description: Edges
type: string
name:
type: string
type: object
repo.ItemOut:
properties:
attachments:
items:
$ref: '#/definitions/repo.ItemAttachment'
type: array
createdAt:
type: string
description:
type: string
id:
type: string
insured:
type: boolean
labels:
items:
$ref: '#/definitions/repo.LabelSummary'
type: array
lifetimeWarranty:
description: Warranty
type: boolean
location:
$ref: '#/definitions/repo.LocationSummary'
description: Edges
manufacturer:
type: string
modelNumber:
type: string
name:
type: string
notes:
description: Extras
type: string
purchaseFrom:
type: string
purchasePrice:
example: "0"
type: string
purchaseTime:
description: Purchase
type: string
quantity:
type: integer
serialNumber:
type: string
soldNotes:
type: string
soldPrice:
example: "0"
type: string
soldTime:
description: Sold
type: string
soldTo:
type: string
updatedAt:
type: string
warrantyDetails:
type: string
warrantyExpires:
type: string
type: object
repo.ItemSummary:
properties:
createdAt:
type: string
description:
type: string
id:
type: string
insured:
type: boolean
labels:
items:
$ref: '#/definitions/repo.LabelSummary'
type: array
location:
$ref: '#/definitions/repo.LocationSummary'
description: Edges
name:
type: string
quantity:
type: integer
updatedAt:
type: string
type: object
repo.ItemUpdate:
properties:
description:
type: string
id:
type: string
insured:
type: boolean
labelIds:
items:
type: string
type: array
lifetimeWarranty:
description: Warranty
type: boolean
locationId:
description: Edges
type: string
manufacturer:
type: string
modelNumber:
type: string
name:
type: string
notes:
description: Extras
type: string
purchaseFrom:
type: string
purchasePrice:
example: "0"
type: string
purchaseTime:
description: Purchase
type: string
quantity:
type: integer
serialNumber:
description: Identifications
type: string
soldNotes:
type: string
soldPrice:
example: "0"
type: string
soldTime:
description: Sold
type: string
soldTo:
type: string
warrantyDetails:
type: string
warrantyExpires:
type: string
type: object
repo.LabelCreate:
properties:
color:
type: string
description:
type: string
name:
type: string
type: object
repo.LabelOut:
properties:
createdAt:
type: string
description:
type: string
id:
type: string
items:
items:
$ref: '#/definitions/repo.ItemSummary'
type: array
name:
type: string
updatedAt:
type: string
type: object
repo.LabelSummary:
properties:
createdAt:
type: string
description:
type: string
id:
type: string
name:
type: string
updatedAt:
type: string
type: object
repo.LocationCreate:
properties:
description:
type: string
name:
type: string
type: object
repo.LocationOut:
properties:
createdAt:
type: string
description:
type: string
id:
type: string
items:
items:
$ref: '#/definitions/repo.ItemSummary'
type: array
name:
type: string
updatedAt:
type: string
type: object
repo.LocationOutCount:
properties:
createdAt:
type: string
description:
type: string
id:
type: string
itemCount:
type: integer
name:
type: string
updatedAt:
type: string
type: object
repo.LocationSummary:
properties:
createdAt:
type: string
description:
type: string
id:
type: string
name:
type: string
updatedAt:
type: string
type: object
repo.UserOut:
properties:
email:
type: string
groupId:
type: string
groupName:
type: string
id:
type: string
isSuperuser:
type: boolean
name:
type: string
type: object
repo.UserUpdate:
properties:
email:
type: string
name:
type: string
type: object
server.Result:
properties:
details: {}
@ -21,10 +318,21 @@ definitions:
reason:
type: string
type: object
types.ApiSummary:
services.UserRegistration:
properties:
email:
type: string
groupName:
type: string
name:
type: string
password:
type: string
type: object
v1.ApiSummary:
properties:
build:
$ref: '#/definitions/types.Build'
$ref: '#/definitions/v1.Build'
health:
type: boolean
message:
@ -36,7 +344,7 @@ definitions:
type: string
type: array
type: object
types.Build:
v1.Build:
properties:
buildTime:
type: string
@ -45,367 +353,18 @@ definitions:
version:
type: string
type: object
types.DocumentOut:
properties:
id:
type: string
path:
type: string
title:
type: string
type: object
types.ItemAttachment:
properties:
createdAt:
type: string
document:
$ref: '#/definitions/types.DocumentOut'
id:
type: string
type:
type: string
updatedAt:
type: string
type: object
types.ItemAttachmentToken:
v1.ItemAttachmentToken:
properties:
token:
type: string
type: object
types.ItemAttachmentUpdate:
properties:
title:
type: string
type:
type: string
type: object
types.ItemCreate:
properties:
description:
type: string
labelIds:
items:
type: string
type: array
locationId:
description: Edges
type: string
name:
type: string
type: object
types.ItemOut:
properties:
attachments:
items:
$ref: '#/definitions/types.ItemAttachment'
type: array
createdAt:
type: string
description:
type: string
id:
type: string
insured:
type: boolean
labels:
items:
$ref: '#/definitions/types.LabelSummary'
type: array
lifetimeWarranty:
description: Warranty
type: boolean
location:
$ref: '#/definitions/types.LocationSummary'
description: Edges
manufacturer:
type: string
modelNumber:
type: string
name:
type: string
notes:
description: Extras
type: string
purchaseFrom:
type: string
purchasePrice:
example: "0"
type: string
purchaseTime:
description: Purchase
type: string
quantity:
type: integer
serialNumber:
description: Identifications
type: string
soldNotes:
type: string
soldPrice:
example: "0"
type: string
soldTime:
description: Sold
type: string
soldTo:
type: string
updatedAt:
type: string
warrantyDetails:
type: string
warrantyExpires:
type: string
type: object
types.ItemSummary:
properties:
createdAt:
type: string
description:
type: string
id:
type: string
insured:
type: boolean
labels:
items:
$ref: '#/definitions/types.LabelSummary'
type: array
lifetimeWarranty:
description: Warranty
type: boolean
location:
$ref: '#/definitions/types.LocationSummary'
description: Edges
manufacturer:
type: string
modelNumber:
type: string
name:
type: string
notes:
description: Extras
type: string
purchaseFrom:
type: string
purchasePrice:
example: "0"
type: string
purchaseTime:
description: Purchase
type: string
quantity:
type: integer
serialNumber:
description: Identifications
type: string
soldNotes:
type: string
soldPrice:
example: "0"
type: string
soldTime:
description: Sold
type: string
soldTo:
type: string
updatedAt:
type: string
warrantyDetails:
type: string
warrantyExpires:
type: string
type: object
types.ItemUpdate:
properties:
description:
type: string
id:
type: string
insured:
type: boolean
labelIds:
items:
type: string
type: array
lifetimeWarranty:
description: Warranty
type: boolean
locationId:
description: Edges
type: string
manufacturer:
type: string
modelNumber:
type: string
name:
type: string
notes:
description: Extras
type: string
purchaseFrom:
type: string
purchasePrice:
example: "0"
type: string
purchaseTime:
description: Purchase
type: string
quantity:
type: integer
serialNumber:
description: Identifications
type: string
soldNotes:
type: string
soldPrice:
example: "0"
type: string
soldTime:
description: Sold
type: string
soldTo:
type: string
warrantyDetails:
type: string
warrantyExpires:
type: string
type: object
types.LabelCreate:
properties:
color:
type: string
description:
type: string
name:
type: string
type: object
types.LabelOut:
properties:
createdAt:
type: string
description:
type: string
id:
type: string
items:
items:
$ref: '#/definitions/types.ItemSummary'
type: array
name:
type: string
updatedAt:
type: string
type: object
types.LabelSummary:
properties:
createdAt:
type: string
description:
type: string
id:
type: string
name:
type: string
updatedAt:
type: string
type: object
types.LocationCount:
properties:
createdAt:
type: string
description:
type: string
id:
type: string
itemCount:
type: integer
name:
type: string
updatedAt:
type: string
type: object
types.LocationCreate:
properties:
description:
type: string
name:
type: string
type: object
types.LocationOut:
properties:
createdAt:
type: string
description:
type: string
id:
type: string
items:
items:
$ref: '#/definitions/types.ItemSummary'
type: array
name:
type: string
updatedAt:
type: string
type: object
types.LocationSummary:
properties:
createdAt:
type: string
description:
type: string
id:
type: string
name:
type: string
updatedAt:
type: string
type: object
types.TokenResponse:
v1.TokenResponse:
properties:
expiresAt:
type: string
token:
type: string
type: object
types.UserIn:
properties:
email:
type: string
name:
type: string
password:
type: string
type: object
types.UserOut:
properties:
email:
type: string
groupId:
type: string
groupName:
type: string
id:
type: string
isSuperuser:
type: boolean
name:
type: string
type: object
types.UserRegistration:
properties:
groupName:
type: string
user:
$ref: '#/definitions/types.UserIn'
type: object
types.UserUpdate:
properties:
email:
type: string
name:
type: string
type: object
info:
contact:
name: Don't
@ -430,7 +389,7 @@ paths:
- properties:
items:
items:
$ref: '#/definitions/types.ItemSummary'
$ref: '#/definitions/repo.ItemSummary'
type: array
type: object
security:
@ -445,14 +404,14 @@ paths:
name: payload
required: true
schema:
$ref: '#/definitions/types.ItemCreate'
$ref: '#/definitions/repo.ItemCreate'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/types.ItemSummary'
$ref: '#/definitions/repo.ItemSummary'
security:
- Bearer: []
summary: Create a new item
@ -489,7 +448,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/types.ItemOut'
$ref: '#/definitions/repo.ItemOut'
security:
- Bearer: []
summary: Gets a item and fields
@ -507,14 +466,14 @@ paths:
name: payload
required: true
schema:
$ref: '#/definitions/types.ItemUpdate'
$ref: '#/definitions/repo.ItemUpdate'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/types.ItemOut'
$ref: '#/definitions/repo.ItemOut'
security:
- Bearer: []
summary: updates a item
@ -549,7 +508,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/types.ItemOut'
$ref: '#/definitions/repo.ItemOut'
"422":
description: Unprocessable Entity
schema:
@ -600,7 +559,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/types.ItemAttachmentToken'
$ref: '#/definitions/v1.ItemAttachmentToken'
security:
- Bearer: []
summary: retrieves an attachment for an item
@ -623,12 +582,12 @@ paths:
name: payload
required: true
schema:
$ref: '#/definitions/types.ItemAttachmentUpdate'
$ref: '#/definitions/repo.ItemAttachmentUpdate'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/types.ItemOut'
$ref: '#/definitions/repo.ItemOut'
security:
- Bearer: []
summary: retrieves an attachment for an item
@ -688,7 +647,7 @@ paths:
- properties:
items:
items:
$ref: '#/definitions/types.LabelOut'
$ref: '#/definitions/repo.LabelOut'
type: array
type: object
security:
@ -703,14 +662,14 @@ paths:
name: payload
required: true
schema:
$ref: '#/definitions/types.LabelCreate'
$ref: '#/definitions/repo.LabelCreate'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/types.LabelSummary'
$ref: '#/definitions/repo.LabelSummary'
security:
- Bearer: []
summary: Create a new label
@ -747,7 +706,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/types.LabelOut'
$ref: '#/definitions/repo.LabelOut'
security:
- Bearer: []
summary: Gets a label and fields
@ -766,7 +725,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/types.LabelOut'
$ref: '#/definitions/repo.LabelOut'
security:
- Bearer: []
summary: updates a label
@ -785,7 +744,7 @@ paths:
- properties:
items:
items:
$ref: '#/definitions/types.LocationCount'
$ref: '#/definitions/repo.LocationOutCount'
type: array
type: object
security:
@ -800,14 +759,14 @@ paths:
name: payload
required: true
schema:
$ref: '#/definitions/types.LocationCreate'
$ref: '#/definitions/repo.LocationCreate'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/types.LocationSummary'
$ref: '#/definitions/repo.LocationSummary'
security:
- Bearer: []
summary: Create a new location
@ -844,7 +803,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/types.LocationOut'
$ref: '#/definitions/repo.LocationOut'
security:
- Bearer: []
summary: Gets a location and fields
@ -863,7 +822,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/types.LocationOut'
$ref: '#/definitions/repo.LocationOut'
security:
- Bearer: []
summary: updates a location
@ -877,7 +836,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/types.ApiSummary'
$ref: '#/definitions/v1.ApiSummary'
summary: Retrieves the basic information about the API
tags:
- Base
@ -903,7 +862,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/types.TokenResponse'
$ref: '#/definitions/v1.TokenResponse'
summary: User Login
tags:
- Authentication
@ -938,7 +897,7 @@ paths:
name: payload
required: true
schema:
$ref: '#/definitions/types.UserRegistration'
$ref: '#/definitions/services.UserRegistration'
produces:
- application/json
responses:
@ -970,7 +929,7 @@ paths:
- $ref: '#/definitions/server.Result'
- properties:
item:
$ref: '#/definitions/types.UserOut'
$ref: '#/definitions/repo.UserOut'
type: object
security:
- Bearer: []
@ -984,7 +943,7 @@ paths:
name: payload
required: true
schema:
$ref: '#/definitions/types.UserUpdate'
$ref: '#/definitions/repo.UserUpdate'
produces:
- application/json
responses:
@ -995,7 +954,7 @@ paths:
- $ref: '#/definitions/server.Result'
- properties:
item:
$ref: '#/definitions/types.UserUpdate'
$ref: '#/definitions/repo.UserUpdate'
type: object
security:
- Bearer: []

View file

@ -81,8 +81,8 @@ func run(cfg *config.Config) error {
}
app.db = c
app.repos = repo.EntAllRepos(c)
app.services = services.NewServices(app.repos, cfg.Storage.Data)
app.repos = repo.EntAllRepos(c, cfg.Storage.Data)
app.services = services.NewServices(app.repos)
// =========================================================================
// Start Server

View file

@ -57,7 +57,7 @@ func (a *app) mwAuthToken(next http.Handler) http.Handler {
return
}
r = r.WithContext(services.SetUserCtx(r.Context(), usr, requestToken))
r = r.WithContext(services.SetUserCtx(r.Context(), &usr, requestToken))
next.ServeHTTP(w, r)
})

View file

@ -14,7 +14,6 @@ import (
_ "github.com/hay-kot/homebox/backend/app/api/docs"
v1 "github.com/hay-kot/homebox/backend/app/api/v1"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/rs/zerolog/log"
httpSwagger "github.com/swaggo/http-swagger" // http-swagger middleware
)
@ -45,7 +44,7 @@ func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux {
v1Base := v1.BaseUrlFunc(prefix)
v1Ctrl := v1.NewControllerV1(a.services, v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize))
{
r.Get(v1Base("/status"), v1Ctrl.HandleBase(func() bool { return true }, types.Build{
r.Get(v1Base("/status"), v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
Version: Version,
Commit: Commit,
BuildTime: BuildTime,

View file

@ -4,7 +4,6 @@ import (
"net/http"
"github.com/hay-kot/homebox/backend/internal/services"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/pkgs/server"
)
@ -19,6 +18,22 @@ type V1Controller struct {
maxUploadSize int64
}
type (
Build struct {
Version string `json:"version"`
Commit string `json:"commit"`
BuildTime string `json:"buildTime"`
}
ApiSummary struct {
Healthy bool `json:"health"`
Versions []string `json:"versions"`
Title string `json:"title"`
Message string `json:"message"`
Build Build
}
)
func BaseUrlFunc(prefix string) func(s string) string {
v1Base := prefix + "/v1"
prefixFunc := func(s string) string {
@ -42,11 +57,11 @@ type ReadyFunc func() bool
// @Summary Retrieves the basic information about the API
// @Tags Base
// @Produce json
// @Success 200 {object} types.ApiSummary
// @Success 200 {object} ApiSummary
// @Router /v1/status [GET]
func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build types.Build) http.HandlerFunc {
func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
server.Respond(w, http.StatusOK, types.ApiSummary{
server.Respond(w, http.StatusOK, ApiSummary{
Healthy: ready(),
Title: "Go API Template",
Message: "Welcome to the Go API Template Application!",

View file

@ -1,47 +0,0 @@
package v1
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/stretchr/testify/assert"
)
func Test_NewHandlerV1(t *testing.T) {
v1Base := BaseUrlFunc("/testing/v1")
ctrl := NewControllerV1(mockHandler.svc)
assert.NotNil(t, ctrl)
assert.Equal(t, "/testing/v1/v1/abc123", v1Base("/abc123"))
assert.Equal(t, "/testing/v1/v1/abc123", v1Base("/abc123"))
}
func TestHandlersv1_HandleBase(t *testing.T) {
// Setup
hdlrFunc := mockHandler.HandleBase(func() bool { return true }, types.Build{
Version: "0.1.0",
Commit: "HEAD",
BuildTime: "now",
})
// Call Handler Func
rr := httptest.NewRecorder()
hdlrFunc(rr, nil)
// Validate Status Code
if rr.Code != http.StatusOK {
t.Errorf("Expected status code to be %d, got %d", http.StatusOK, rr.Code)
}
// Validate Json Payload
expected := `{"health":true,"versions":null,"title":"Go API Template","message":"Welcome to the Go API Template Application!","Build":{"version":"0.1.0","commit":"HEAD","buildTime":"now"}}`
if rr.Body.String() != expected {
t.Errorf("Expected json to be %s, got %s", expected, rr.Body.String())
}
}

View file

@ -1,53 +0,0 @@
package v1
import (
"context"
"testing"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/internal/mocks"
"github.com/hay-kot/homebox/backend/internal/mocks/factories"
"github.com/hay-kot/homebox/backend/internal/types"
)
var mockHandler = &V1Controller{}
var users = []*ent.User{}
func userPool() func() {
create := []types.UserCreate{
factories.UserFactory(),
factories.UserFactory(),
factories.UserFactory(),
factories.UserFactory(),
}
userOut := []*ent.User{}
for _, user := range create {
usrOut, _ := mockHandler.svc.Admin.Create(context.Background(), user)
userOut = append(userOut, usrOut)
}
users = userOut
purge := func() {
_ = mockHandler.svc.Admin.DeleteAll(context.Background())
}
return purge
}
func TestMain(m *testing.M) {
// Set Handler Vars
repos, closeDb := mocks.GetEntRepos()
mockHandler.svc = mocks.GetMockServices(repos)
defer func() {
_ = closeDb()
}()
purge := userPool()
defer purge()
m.Run()
}

View file

@ -5,8 +5,8 @@ import (
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/internal/services"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/rs/zerolog/log"
)
@ -21,12 +21,12 @@ and makes it a little more consistent when error handling and logging.
// from the context. If either of these fail, it will return an error. When an error
// occurs it will also write the error to the response. As such, if an error is returned
// from this function you can return immediately without writing to the response.
func (ctrl *V1Controller) partialParseIdAndUser(w http.ResponseWriter, r *http.Request) (uuid.UUID, *types.UserOut, error) {
func (ctrl *V1Controller) partialParseIdAndUser(w http.ResponseWriter, r *http.Request) (uuid.UUID, *repo.UserOut, error) {
uid, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
log.Err(err).Msg("failed to parse id")
server.RespondError(w, http.StatusBadRequest, err)
return uuid.Nil, nil, err
return uuid.Nil, &repo.UserOut{}, err
}
user := services.UseUserCtx(r.Context())

View file

@ -3,13 +3,25 @@ package v1
import (
"errors"
"net/http"
"time"
"github.com/hay-kot/homebox/backend/internal/services"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/rs/zerolog/log"
)
type (
TokenResponse struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expiresAt"`
}
LoginForm struct {
Username string `json:"username"`
Password string `json:"password"`
}
)
// HandleAuthLogin godoc
// @Summary User Login
// @Tags Authentication
@ -18,11 +30,11 @@ import (
// @Param username formData string false "string" example(admin@admin.com)
// @Param password formData string false "string" example(admin)
// @Produce json
// @Success 200 {object} types.TokenResponse
// @Success 200 {object} TokenResponse
// @Router /v1/users/login [POST]
func (ctrl *V1Controller) HandleAuthLogin() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
loginForm := &types.LoginForm{}
loginForm := &LoginForm{}
if r.Header.Get("Content-Type") == server.ContentFormUrlEncoded {
err := r.ParseForm()
@ -59,9 +71,9 @@ func (ctrl *V1Controller) HandleAuthLogin() http.HandlerFunc {
return
}
server.Respond(w, http.StatusOK, types.TokenResponse{
BearerToken: "Bearer " + newToken.Raw,
ExpiresAt: newToken.ExpiresAt,
server.Respond(w, http.StatusOK, TokenResponse{
Token: "Bearer " + newToken.Raw,
ExpiresAt: newToken.ExpiresAt,
})
}
}

View file

@ -4,8 +4,8 @@ import (
"encoding/csv"
"net/http"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/internal/services"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/rs/zerolog/log"
)
@ -14,7 +14,7 @@ import (
// @Summary Get All Items
// @Tags Items
// @Produce json
// @Success 200 {object} server.Results{items=[]types.ItemSummary}
// @Success 200 {object} server.Results{items=[]repo.ItemSummary}
// @Router /v1/items [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleItemsGetAll() http.HandlerFunc {
@ -34,13 +34,13 @@ func (ctrl *V1Controller) HandleItemsGetAll() http.HandlerFunc {
// @Summary Create a new item
// @Tags Items
// @Produce json
// @Param payload body types.ItemCreate true "Item Data"
// @Success 200 {object} types.ItemSummary
// @Param payload body repo.ItemCreate true "Item Data"
// @Success 200 {object} repo.ItemSummary
// @Router /v1/items [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleItemsCreate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
createData := types.ItemCreate{}
createData := repo.ItemCreate{}
if err := server.Decode(r, &createData); err != nil {
log.Err(err).Msg("failed to decode request body")
server.RespondError(w, http.StatusInternalServerError, err)
@ -90,7 +90,7 @@ func (ctrl *V1Controller) HandleItemDelete() http.HandlerFunc {
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Success 200 {object} types.ItemOut
// @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc {
@ -114,14 +114,14 @@ func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc {
// @Summary updates a item
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Param payload body types.ItemUpdate true "Item Data"
// @Success 200 {object} types.ItemOut
// @Param id path string true "Item ID"
// @Param payload body repo.ItemUpdate true "Item Data"
// @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleItemUpdate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
body := types.ItemUpdate{}
body := repo.ItemUpdate{}
if err := server.Decode(r, &body); err != nil {
log.Err(err).Msg("failed to decode request body")
server.RespondError(w, http.StatusInternalServerError, err)

View file

@ -4,17 +4,22 @@ import (
"errors"
"fmt"
"net/http"
"path/filepath"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent/attachment"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/internal/services"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/rs/zerolog/log"
)
type (
ItemAttachmentToken struct {
Token string `json:"token"`
}
)
// HandleItemsImport godocs
// @Summary imports items into the database
// @Tags Items
@ -23,7 +28,7 @@ import (
// @Param file formData file true "File attachment"
// @Param type formData string true "Type of file"
// @Param name formData string true "name of the file including extension"
// @Success 200 {object} types.ItemOut
// @Success 200 {object} repo.ItemOut
// @Failure 422 {object} []server.ValidationError
// @Router /v1/items/{id}/attachments [POST]
// @Security Bearer
@ -105,7 +110,7 @@ func (ctrl *V1Controller) HandleItemAttachmentDownload() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := server.GetParam(r, "token", "")
path, err := ctrl.svc.Items.AttachmentPath(r.Context(), token)
doc, err := ctrl.svc.Items.AttachmentPath(r.Context(), token)
if err != nil {
log.Err(err).Msg("failed to get attachment")
@ -113,9 +118,9 @@ func (ctrl *V1Controller) HandleItemAttachmentDownload() http.HandlerFunc {
return
}
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filepath.Base(path)))
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", doc.Title))
w.Header().Set("Content-Type", "application/octet-stream")
http.ServeFile(w, r, path)
http.ServeFile(w, r, doc.Path)
}
}
@ -125,7 +130,7 @@ func (ctrl *V1Controller) HandleItemAttachmentDownload() http.HandlerFunc {
// @Produce application/octet-stream
// @Param id path string true "Item ID"
// @Param attachment_id path string true "Attachment ID"
// @Success 200 {object} types.ItemAttachmentToken
// @Success 200 {object} ItemAttachmentToken
// @Router /v1/items/{id}/attachments/{attachment_id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentToken() http.HandlerFunc {
@ -147,10 +152,10 @@ func (ctrl *V1Controller) HandleItemAttachmentDelete() http.HandlerFunc {
// HandleItemAttachmentUpdate godocs
// @Summary retrieves an attachment for an item
// @Tags Items
// @Param id path string true "Item ID"
// @Param attachment_id path string true "Attachment ID"
// @Param payload body types.ItemAttachmentUpdate true "Attachment Update"
// @Success 200 {object} types.ItemOut
// @Param id path string true "Item ID"
// @Param attachment_id path string true "Attachment ID"
// @Param payload body repo.ItemAttachmentUpdate true "Attachment Update"
// @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id}/attachments/{attachment_id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentUpdate() http.HandlerFunc {
@ -201,7 +206,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
}
}
server.Respond(w, http.StatusOK, types.ItemAttachmentToken{Token: token})
server.Respond(w, http.StatusOK, ItemAttachmentToken{Token: token})
// Delete Attachment Handler
case http.MethodDelete:
@ -216,7 +221,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
// Update Attachment Handler
case http.MethodPut:
var attachment types.ItemAttachmentUpdate
var attachment repo.ItemAttachmentUpdate
err = server.Decode(r, &attachment)
if err != nil {
log.Err(err).Msg("failed to decode attachment")

View file

@ -4,8 +4,8 @@ import (
"net/http"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/internal/services"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/rs/zerolog/log"
)
@ -14,7 +14,7 @@ import (
// @Summary Get All Labels
// @Tags Labels
// @Produce json
// @Success 200 {object} server.Results{items=[]types.LabelOut}
// @Success 200 {object} server.Results{items=[]repo.LabelOut}
// @Router /v1/labels [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleLabelsGetAll() http.HandlerFunc {
@ -34,13 +34,13 @@ func (ctrl *V1Controller) HandleLabelsGetAll() http.HandlerFunc {
// @Summary Create a new label
// @Tags Labels
// @Produce json
// @Param payload body types.LabelCreate true "Label Data"
// @Success 200 {object} types.LabelSummary
// @Param payload body repo.LabelCreate true "Label Data"
// @Success 200 {object} repo.LabelSummary
// @Router /v1/labels [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleLabelsCreate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
createData := types.LabelCreate{}
createData := repo.LabelCreate{}
if err := server.Decode(r, &createData); err != nil {
log.Err(err).Msg("error decoding label create data")
server.RespondError(w, http.StatusInternalServerError, err)
@ -90,7 +90,7 @@ func (ctrl *V1Controller) HandleLabelDelete() http.HandlerFunc {
// @Tags Labels
// @Produce json
// @Param id path string true "Label ID"
// @Success 200 {object} types.LabelOut
// @Success 200 {object} repo.LabelOut
// @Router /v1/labels/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleLabelGet() http.HandlerFunc {
@ -122,12 +122,12 @@ func (ctrl *V1Controller) HandleLabelGet() http.HandlerFunc {
// @Tags Labels
// @Produce json
// @Param id path string true "Label ID"
// @Success 200 {object} types.LabelOut
// @Success 200 {object} repo.LabelOut
// @Router /v1/labels/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleLabelUpdate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
body := types.LabelUpdate{}
body := repo.LabelUpdate{}
if err := server.Decode(r, &body); err != nil {
log.Err(err).Msg("error decoding label update data")
server.RespondError(w, http.StatusInternalServerError, err)

View file

@ -4,8 +4,8 @@ import (
"net/http"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/internal/services"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/rs/zerolog/log"
)
@ -14,7 +14,7 @@ import (
// @Summary Get All Locations
// @Tags Locations
// @Produce json
// @Success 200 {object} server.Results{items=[]types.LocationCount}
// @Success 200 {object} server.Results{items=[]repo.LocationOutCount}
// @Router /v1/locations [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleLocationGetAll() http.HandlerFunc {
@ -35,13 +35,13 @@ func (ctrl *V1Controller) HandleLocationGetAll() http.HandlerFunc {
// @Summary Create a new location
// @Tags Locations
// @Produce json
// @Param payload body types.LocationCreate true "Location Data"
// @Success 200 {object} types.LocationSummary
// @Param payload body repo.LocationCreate true "Location Data"
// @Success 200 {object} repo.LocationSummary
// @Router /v1/locations [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleLocationCreate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
createData := types.LocationCreate{}
createData := repo.LocationCreate{}
if err := server.Decode(r, &createData); err != nil {
log.Err(err).Msg("failed to decode location create data")
server.RespondError(w, http.StatusInternalServerError, err)
@ -90,7 +90,7 @@ func (ctrl *V1Controller) HandleLocationDelete() http.HandlerFunc {
// @Tags Locations
// @Produce json
// @Param id path string true "Location ID"
// @Success 200 {object} types.LocationOut
// @Success 200 {object} repo.LocationOut
// @Router /v1/locations/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleLocationGet() http.HandlerFunc {
@ -105,12 +105,16 @@ func (ctrl *V1Controller) HandleLocationGet() http.HandlerFunc {
if ent.IsNotFound(err) {
log.Err(err).
Str("id", uid.String()).
Str("gid", user.GroupID.String()).
Msg("location not found")
server.RespondError(w, http.StatusNotFound, err)
return
}
log.Err(err).Msg("failed to get location")
log.Err(err).
Str("id", uid.String()).
Str("gid", user.GroupID.String()).
Msg("failed to get location")
server.RespondServerError(w)
return
}
@ -123,12 +127,12 @@ func (ctrl *V1Controller) HandleLocationGet() http.HandlerFunc {
// @Tags Locations
// @Produce json
// @Param id path string true "Location ID"
// @Success 200 {object} types.LocationOut
// @Success 200 {object} repo.LocationOut
// @Router /v1/locations/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleLocationUpdate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
body := types.LocationUpdate{}
body := repo.LocationUpdate{}
if err := server.Decode(r, &body); err != nil {
log.Err(err).Msg("failed to decode location update data")
server.RespondError(w, http.StatusInternalServerError, err)

View file

@ -4,8 +4,8 @@ import (
"net/http"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/internal/services"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/rs/zerolog/log"
)
@ -14,12 +14,12 @@ import (
// @Summary Get the current user
// @Tags User
// @Produce json
// @Param payload body types.UserRegistration true "User Data"
// @Param payload body services.UserRegistration true "User Data"
// @Success 204
// @Router /v1/users/register [Post]
func (ctrl *V1Controller) HandleUserRegistration() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
regData := types.UserRegistration{}
regData := services.UserRegistration{}
if err := server.Decode(r, &regData); err != nil {
log.Err(err).Msg("failed to decode user registration data")
@ -29,6 +29,7 @@ func (ctrl *V1Controller) HandleUserRegistration() http.HandlerFunc {
_, err := ctrl.svc.User.RegisterUser(r.Context(), regData)
if err != nil {
log.Err(err).Msg("failed to register user")
server.RespondError(w, http.StatusInternalServerError, err)
return
}
@ -41,7 +42,7 @@ func (ctrl *V1Controller) HandleUserRegistration() http.HandlerFunc {
// @Summary Get the current user
// @Tags User
// @Produce json
// @Success 200 {object} server.Result{item=types.UserOut}
// @Success 200 {object} server.Result{item=repo.UserOut}
// @Router /v1/users/self [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleUserSelf() http.HandlerFunc {
@ -62,13 +63,13 @@ func (ctrl *V1Controller) HandleUserSelf() http.HandlerFunc {
// @Summary Update the current user
// @Tags User
// @Produce json
// @Param payload body types.UserUpdate true "User Data"
// @Success 200 {object} server.Result{item=types.UserUpdate}
// @Param payload body repo.UserUpdate true "User Data"
// @Success 200 {object} server.Result{item=repo.UserUpdate}
// @Router /v1/users/self [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleUserSelfUpdate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateData := types.UserUpdate{}
updateData := repo.UserUpdate{}
if err := server.Decode(r, &updateData); err != nil {
log.Err(err).Msg("failed to decode user update data")
server.RespondError(w, http.StatusBadRequest, err)

View file

@ -1,13 +1,13 @@
package factories
import (
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/pkgs/faker"
)
func UserFactory() types.UserCreate {
func UserFactory() repo.UserCreate {
f := faker.NewFaker()
return types.UserCreate{
return repo.UserCreate{
Name: f.Str(10),
Email: f.Email(),
Password: f.Str(10),

View file

@ -1,10 +0,0 @@
package mocks
import (
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/internal/services"
)
func GetMockServices(repos *repo.AllRepos) *services.AllServices {
return services.NewServices(repos, "/tmp/homebox")
}

View file

@ -1,22 +0,0 @@
package mocks
import (
"context"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/internal/repo"
_ "github.com/mattn/go-sqlite3"
)
func GetEntRepos() (*repo.AllRepos, func() error) {
c, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
panic(err)
}
if err := c.Schema.Create(context.Background()); err != nil {
panic(err)
}
return repo.EntAllRepos(c), c.Close
}

View file

@ -1,6 +1,9 @@
package repo
import "github.com/google/uuid"
import (
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/pkgs/set"
)
// HasID is an interface to entities that have an ID uuid.UUID field and a GetID() method.
// This interface is fulfilled by all entities generated by entgo.io/ent via a custom template
@ -8,55 +11,11 @@ type HasID interface {
GetID() uuid.UUID
}
// IDSet is a utility set-like type for working with sets of uuid.UUIDs within a repository
// instance. Most useful for comparing lists of UUIDs for processing relationship
// IDs and remove/adding relationships as required.
//
// # See how ItemRepo uses it to manage the Labels-To-Items relationship
//
// NOTE: may be worth moving this to a more generic package/set implementation
// or use a 3rd party set library, but this is good enough for now
type IDSet struct {
mp map[uuid.UUID]struct{}
}
func NewIDSet(l int) *IDSet {
return &IDSet{
mp: make(map[uuid.UUID]struct{}, l),
}
}
func EntitiesToIDSet[T HasID](entities []T) *IDSet {
s := NewIDSet(len(entities))
func newIDSet[T HasID](entities []T) set.Set[uuid.UUID] {
uuids := make([]uuid.UUID, 0, len(entities))
for _, e := range entities {
s.Add(e.GetID())
uuids = append(uuids, e.GetID())
}
return s
}
func (t *IDSet) Slice() []uuid.UUID {
s := make([]uuid.UUID, 0, len(t.mp))
for k := range t.mp {
s = append(s, k)
}
return s
}
func (t *IDSet) Add(ids ...uuid.UUID) {
for _, id := range ids {
t.mp[id] = struct{}{}
}
}
func (t *IDSet) Has(id uuid.UUID) bool {
_, ok := t.mp[id]
return ok
}
func (t *IDSet) Len() int {
return len(t.mp)
}
func (t *IDSet) Remove(id uuid.UUID) {
delete(t.mp, id)
return set.New(uuids...)
}

View file

@ -18,7 +18,7 @@ var (
tClient *ent.Client
tRepos *AllRepos
tUser *ent.User
tUser UserOut
tGroup *ent.Group
)
@ -53,7 +53,7 @@ func TestMain(m *testing.M) {
}
tClient = client
tRepos = EntAllRepos(tClient)
tRepos = EntAllRepos(tClient, os.TempDir())
defer client.Close()
bootstrap()

View file

@ -0,0 +1,52 @@
package repo
// mapTErrFunc is a factory function that returns a mapper function that
// wraps the given mapper function but first will check for an error and
// return the error if present.
//
// Helpful for wrapping database calls that return both a value and an error
func mapTErrFunc[T any, Y any](fn func(T) Y) func(T, error) (Y, error) {
return func(t T, err error) (Y, error) {
if err != nil {
var zero Y
return zero, err
}
return fn(t), nil
}
}
// TODO: Future Usage
// func mapEachFunc[T any, Y any](fn func(T) Y) func([]T) []Y {
// return func(items []T) []Y {
// result := make([]Y, len(items))
// for i, item := range items {
// result[i] = fn(item)
// }
// return result
// }
// }
func mapTEachErrFunc[T any, Y any](fn func(T) Y) func([]T, error) ([]Y, error) {
return func(items []T, err error) ([]Y, error) {
if err != nil {
return nil, err
}
result := make([]Y, len(items))
for i, item := range items {
result[i] = fn(item)
}
return result, nil
}
}
func mapEach[T any, U any](items []T, fn func(T) U) []U {
result := make([]U, len(items))
for i, item := range items {
result[i] = fn(item)
}
return result
}

View file

@ -7,7 +7,6 @@ import (
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/ent/documenttoken"
"github.com/hay-kot/homebox/backend/internal/types"
)
// DocumentTokensRepository is a repository for Document entity
@ -15,7 +14,35 @@ type DocumentTokensRepository struct {
db *ent.Client
}
func (r *DocumentTokensRepository) Create(ctx context.Context, data types.DocumentTokenCreate) (*ent.DocumentToken, error) {
type (
DocumentToken struct {
ID uuid.UUID `json:"-"`
TokenHash []byte `json:"tokenHash"`
ExpiresAt time.Time `json:"expiresAt"`
DocumentID uuid.UUID `json:"documentId"`
}
DocumentTokenCreate struct {
TokenHash []byte `json:"tokenHash"`
DocumentID uuid.UUID `json:"documentId"`
ExpiresAt time.Time `json:"expiresAt"`
}
)
var (
mapDocumentTokenErr = mapTErrFunc(mapDocumentToken)
)
func mapDocumentToken(e *ent.DocumentToken) DocumentToken {
return DocumentToken{
ID: e.ID,
TokenHash: e.Token,
ExpiresAt: e.ExpiresAt,
DocumentID: e.Edges.Document.ID,
}
}
func (r *DocumentTokensRepository) Create(ctx context.Context, data DocumentTokenCreate) (DocumentToken, error) {
result, err := r.db.DocumentToken.Create().
SetDocumentID(data.DocumentID).
SetToken(data.TokenHash).
@ -23,13 +50,13 @@ func (r *DocumentTokensRepository) Create(ctx context.Context, data types.Docume
Save(ctx)
if err != nil {
return nil, err
return DocumentToken{}, err
}
return r.db.DocumentToken.Query().
return mapDocumentTokenErr(r.db.DocumentToken.Query().
Where(documenttoken.ID(result.ID)).
WithDocument().
Only(ctx)
Only(ctx))
}
func (r *DocumentTokensRepository) PurgeExpiredTokens(ctx context.Context) (int, error) {

View file

@ -8,7 +8,6 @@ import (
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/ent/documenttoken"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/stretchr/testify/assert"
)
@ -19,7 +18,7 @@ func TestDocumentTokensRepository_Create(t *testing.T) {
type args struct {
ctx context.Context
data types.DocumentTokenCreate
data DocumentTokenCreate
}
tests := []struct {
name string
@ -31,7 +30,7 @@ func TestDocumentTokensRepository_Create(t *testing.T) {
name: "create document token",
args: args{
ctx: context.Background(),
data: types.DocumentTokenCreate{
data: DocumentTokenCreate{
DocumentID: doc.ID,
TokenHash: []byte("token"),
ExpiresAt: expires,
@ -39,7 +38,9 @@ func TestDocumentTokensRepository_Create(t *testing.T) {
},
want: &ent.DocumentToken{
Edges: ent.DocumentTokenEdges{
Document: doc,
Document: &ent.Document{
ID: doc.ID,
},
},
Token: []byte("token"),
ExpiresAt: expires,
@ -50,7 +51,7 @@ func TestDocumentTokensRepository_Create(t *testing.T) {
name: "create document token with empty token",
args: args{
ctx: context.Background(),
data: types.DocumentTokenCreate{
data: DocumentTokenCreate{
DocumentID: doc.ID,
TokenHash: []byte(""),
ExpiresAt: expires,
@ -63,7 +64,7 @@ func TestDocumentTokensRepository_Create(t *testing.T) {
name: "create document token with empty document id",
args: args{
ctx: context.Background(),
data: types.DocumentTokenCreate{
data: DocumentTokenCreate{
DocumentID: uuid.Nil,
TokenHash: []byte("token"),
ExpiresAt: expires,
@ -94,18 +95,18 @@ func TestDocumentTokensRepository_Create(t *testing.T) {
return
}
assert.Equal(t, tt.want.Token, got.Token)
assert.Equal(t, tt.want.Token, got.TokenHash)
assert.WithinDuration(t, tt.want.ExpiresAt, got.ExpiresAt, time.Duration(1)*time.Second)
assert.Equal(t, tt.want.Edges.Document.ID, got.Edges.Document.ID)
assert.Equal(t, tt.want.Edges.Document.ID, got.DocumentID)
})
}
}
func useDocTokens(t *testing.T, num int) []*ent.DocumentToken {
func useDocTokens(t *testing.T, num int) []DocumentToken {
entity := useDocs(t, 1)[0]
results := make([]*ent.DocumentToken, 0, num)
results := make([]DocumentToken, 0, num)
ids := make([]uuid.UUID, 0, num)
t.Cleanup(func() {
@ -115,7 +116,7 @@ func useDocTokens(t *testing.T, num int) []*ent.DocumentToken {
})
for i := 0; i < num; i++ {
e, err := tRepos.DocTokens.Create(context.Background(), types.DocumentTokenCreate{
e, err := tRepos.DocTokens.Create(context.Background(), DocumentTokenCreate{
DocumentID: entity.ID,
TokenHash: []byte(fk.Str(10)),
ExpiresAt: fk.Time(),

View file

@ -2,46 +2,117 @@ package repo
import (
"context"
"errors"
"io"
"os"
"path/filepath"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/ent/document"
"github.com/hay-kot/homebox/backend/ent/group"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/pkgs/pathlib"
)
var (
ErrInvalidDocExtension = errors.New("invalid document extension")
)
// DocumentRepository is a repository for Document entity
type DocumentRepository struct {
db *ent.Client
db *ent.Client
dir string
}
func (r *DocumentRepository) Create(ctx context.Context, gid uuid.UUID, doc types.DocumentCreate) (*ent.Document, error) {
return r.db.Document.Create().
type (
DocumentCreate struct {
Title string `json:"title"`
Content io.Reader `json:"content"`
}
DocumentOut struct {
ID uuid.UUID `json:"id"`
Title string `json:"title"`
Path string `json:"path"`
}
)
func mapDocumentOut(doc *ent.Document) DocumentOut {
return DocumentOut{
ID: doc.ID,
Title: doc.Title,
Path: doc.Path,
}
}
var (
mapDocumentOutErr = mapTErrFunc(mapDocumentOut)
mapDocumentOutEachErr = mapTEachErrFunc(mapDocumentOut)
)
func (r *DocumentRepository) path(gid uuid.UUID, ext string) string {
return pathlib.Safe(filepath.Join(r.dir, gid.String(), "documents", uuid.NewString()+ext))
}
func (r *DocumentRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]DocumentOut, error) {
return mapDocumentOutEachErr(r.db.Document.
Query().
Where(document.HasGroupWith(group.ID(gid))).
All(ctx),
)
}
func (r *DocumentRepository) Get(ctx context.Context, id uuid.UUID) (DocumentOut, error) {
return mapDocumentOutErr(r.db.Document.Get(ctx, id))
}
func (r *DocumentRepository) Create(ctx context.Context, gid uuid.UUID, doc DocumentCreate) (DocumentOut, error) {
ext := filepath.Ext(doc.Title)
if ext == "" {
return DocumentOut{}, ErrInvalidDocExtension
}
path := r.path(gid, ext)
parent := filepath.Dir(path)
err := os.MkdirAll(parent, 0755)
if err != nil {
return DocumentOut{}, err
}
f, err := os.Create(path)
if err != nil {
return DocumentOut{}, err
}
_, err = io.Copy(f, doc.Content)
if err != nil {
return DocumentOut{}, err
}
return mapDocumentOutErr(r.db.Document.Create().
SetGroupID(gid).
SetTitle(doc.Title).
SetPath(doc.Path).
Save(ctx)
SetPath(path).
Save(ctx),
)
}
func (r *DocumentRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]*ent.Document, error) {
return r.db.Document.Query().
Where(document.HasGroupWith(group.ID(gid))).
All(ctx)
}
func (r *DocumentRepository) Get(ctx context.Context, id uuid.UUID) (*ent.Document, error) {
return r.db.Document.Query().
Where(document.ID(id)).
Only(ctx)
}
func (r *DocumentRepository) Update(ctx context.Context, id uuid.UUID, doc types.DocumentUpdate) (*ent.Document, error) {
return r.db.Document.UpdateOneID(id).
SetTitle(doc.Title).
SetPath(doc.Path).
Save(ctx)
func (r *DocumentRepository) Rename(ctx context.Context, id uuid.UUID, title string) (DocumentOut, error) {
return mapDocumentOutErr(r.db.Document.UpdateOneID(id).
SetTitle(title).
Save(ctx))
}
func (r *DocumentRepository) Delete(ctx context.Context, id uuid.UUID) error {
doc, err := r.db.Document.Get(ctx, id)
if err != nil {
return err
}
err = os.Remove(doc.Path)
if err != nil {
return err
}
return r.db.Document.DeleteOneID(id).Exec(ctx)
}

View file

@ -1,110 +1,28 @@
package repo
import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/stretchr/testify/assert"
)
func TestDocumentRepository_Create(t *testing.T) {
type args struct {
ctx context.Context
gid uuid.UUID
doc types.DocumentCreate
}
tests := []struct {
name string
args args
want *ent.Document
wantErr bool
}{
{
name: "create document",
args: args{
ctx: context.Background(),
gid: tGroup.ID,
doc: types.DocumentCreate{
Title: "test document",
Path: "/test/document",
},
},
want: &ent.Document{
Title: "test document",
Path: "/test/document",
},
wantErr: false,
},
{
name: "create document with empty title",
args: args{
ctx: context.Background(),
gid: tGroup.ID,
doc: types.DocumentCreate{
Title: "",
Path: "/test/document",
},
},
want: nil,
wantErr: true,
},
{
name: "create document with empty path",
args: args{
ctx: context.Background(),
gid: tGroup.ID,
doc: types.DocumentCreate{
Title: "test document",
Path: "",
},
},
want: nil,
wantErr: true,
},
}
ids := make([]uuid.UUID, 0, len(tests))
t.Cleanup(func() {
for _, id := range ids {
err := tRepos.Docs.Delete(context.Background(), id)
assert.NoError(t, err)
}
})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tRepos.Docs.Create(tt.args.ctx, tt.args.gid, tt.args.doc)
if (err != nil) != tt.wantErr {
t.Errorf("DocumentRepository.Create() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr {
assert.Error(t, err)
assert.Nil(t, got)
return
}
assert.Equal(t, tt.want.Title, got.Title)
assert.Equal(t, tt.want.Path, got.Path)
ids = append(ids, got.ID)
})
}
}
func useDocs(t *testing.T, num int) []*ent.Document {
func useDocs(t *testing.T, num int) []DocumentOut {
t.Helper()
results := make([]*ent.Document, 0, num)
results := make([]DocumentOut, 0, num)
ids := make([]uuid.UUID, 0, num)
for i := 0; i < num; i++ {
doc, err := tRepos.Docs.Create(context.Background(), tGroup.ID, types.DocumentCreate{
Title: fk.Str(10),
Path: fk.Path(),
doc, err := tRepos.Docs.Create(context.Background(), tGroup.ID, DocumentCreate{
Title: fk.Str(10) + ".md",
Content: bytes.NewReader([]byte(fk.Str(10))),
})
assert.NoError(t, err)
@ -126,77 +44,68 @@ func useDocs(t *testing.T, num int) []*ent.Document {
return results
}
func TestDocumentRepository_GetAll(t *testing.T) {
entities := useDocs(t, 10)
for _, entity := range entities {
assert.NotNil(t, entity)
func TestDocumentRepository_CreateUpdateDelete(t *testing.T) {
temp := t.TempDir()
r := DocumentRepository{
db: tClient,
dir: temp,
}
all, err := tRepos.Docs.GetAll(context.Background(), tGroup.ID)
assert.NoError(t, err)
type args struct {
ctx context.Context
gid uuid.UUID
doc DocumentCreate
}
tests := []struct {
name string
content string
args args
title string
wantErr bool
}{
{
name: "basic create",
title: "test.md",
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
args: args{
ctx: context.Background(),
gid: tGroup.ID,
doc: DocumentCreate{
Title: "test.md",
Content: bytes.NewReader([]byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create Document
got, err := r.Create(tt.args.ctx, tt.args.gid, tt.args.doc)
assert.NoError(t, err)
assert.Equal(t, tt.title, got.Title)
assert.Equal(t, fmt.Sprintf("%s/%s/documents", temp, tt.args.gid), filepath.Dir(got.Path))
assert.Len(t, all, 10)
for _, entity := range all {
assert.NotNil(t, entity)
for _, e := range entities {
if e.ID == entity.ID {
assert.Equal(t, e.Title, entity.Title)
assert.Equal(t, e.Path, entity.Path)
ensureRead := func() {
// Read Document
bts, err := os.ReadFile(got.Path)
assert.NoError(t, err)
assert.Equal(t, tt.content, string(bts))
}
}
}
}
func TestDocumentRepository_Get(t *testing.T) {
entities := useDocs(t, 10)
for _, entity := range entities {
got, err := tRepos.Docs.Get(context.Background(), entity.ID)
assert.NoError(t, err)
assert.Equal(t, entity.ID, got.ID)
assert.Equal(t, entity.Title, got.Title)
assert.Equal(t, entity.Path, got.Path)
}
}
func TestDocumentRepository_Update(t *testing.T) {
entities := useDocs(t, 10)
for _, entity := range entities {
got, err := tRepos.Docs.Get(context.Background(), entity.ID)
assert.NoError(t, err)
assert.Equal(t, entity.ID, got.ID)
assert.Equal(t, entity.Title, got.Title)
assert.Equal(t, entity.Path, got.Path)
}
for _, entity := range entities {
updateData := types.DocumentUpdate{
Title: fk.Str(10),
Path: fk.Path(),
}
updated, err := tRepos.Docs.Update(context.Background(), entity.ID, updateData)
assert.NoError(t, err)
assert.Equal(t, entity.ID, updated.ID)
assert.Equal(t, updateData.Title, updated.Title)
assert.Equal(t, updateData.Path, updated.Path)
}
}
func TestDocumentRepository_Delete(t *testing.T) {
entities := useDocs(t, 10)
for _, entity := range entities {
err := tRepos.Docs.Delete(context.Background(), entity.ID)
assert.NoError(t, err)
_, err = tRepos.Docs.Get(context.Background(), entity.ID)
assert.Error(t, err)
ensureRead()
// Update Document
got, err = r.Rename(tt.args.ctx, got.ID, "__"+tt.title+"__")
assert.NoError(t, err)
assert.Equal(t, "__"+tt.title+"__", got.Title)
ensureRead()
// Delete Document
err = r.Delete(tt.args.ctx, got.ID)
assert.NoError(t, err)
_, err = os.Stat(got.Path)
assert.Error(t, err)
})
}
}

View file

@ -2,6 +2,7 @@ package repo
import (
"context"
"time"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent"
@ -16,6 +17,36 @@ type AttachmentRepo struct {
db *ent.Client
}
type (
ItemAttachment struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Type string `json:"type"`
Document DocumentOut `json:"document"`
}
ItemAttachmentUpdate struct {
ID uuid.UUID `json:"-"`
Type string `json:"type"`
Title string `json:"title"`
}
)
func ToItemAttachment(attachment *ent.Attachment) ItemAttachment {
return ItemAttachment{
ID: attachment.ID,
CreatedAt: attachment.CreatedAt,
UpdatedAt: attachment.UpdatedAt,
Type: attachment.Type.String(),
Document: DocumentOut{
ID: attachment.Edges.Document.ID,
Title: attachment.Edges.Document.Title,
Path: attachment.Edges.Document.Path,
},
}
}
func (r *AttachmentRepo) Create(ctx context.Context, itemId, docId uuid.UUID, typ attachment.Type) (*ent.Attachment, error) {
return r.db.Attachment.Create().
SetType(typ).

View file

@ -2,21 +2,187 @@ package repo
import (
"context"
"time"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/ent/group"
"github.com/hay-kot/homebox/backend/ent/item"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/ent/predicate"
)
type ItemsRepository struct {
db *ent.Client
}
func (e *ItemsRepository) GetOne(ctx context.Context, id uuid.UUID) (*ent.Item, error) {
return e.db.Item.Query().
Where(item.ID(id)).
type (
ItemCreate struct {
ImportRef string `json:"-"`
Name string `json:"name"`
Description string `json:"description"`
// Edges
LocationID uuid.UUID `json:"locationId"`
LabelIDs []uuid.UUID `json:"labelIds"`
}
ItemUpdate struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Quantity int `json:"quantity"`
Insured bool `json:"insured"`
// Edges
LocationID uuid.UUID `json:"locationId"`
LabelIDs []uuid.UUID `json:"labelIds"`
// Identifications
SerialNumber string `json:"serialNumber"`
ModelNumber string `json:"modelNumber"`
Manufacturer string `json:"manufacturer"`
// Warranty
LifetimeWarranty bool `json:"lifetimeWarranty"`
WarrantyExpires time.Time `json:"warrantyExpires"`
WarrantyDetails string `json:"warrantyDetails"`
// Purchase
PurchaseTime time.Time `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"`
PurchasePrice float64 `json:"purchasePrice,string"`
// Sold
SoldTime time.Time `json:"soldTime"`
SoldTo string `json:"soldTo"`
SoldPrice float64 `json:"soldPrice,string"`
SoldNotes string `json:"soldNotes"`
// Extras
Notes string `json:"notes"`
// Fields []*FieldSummary `json:"fields"`
}
ItemSummary struct {
ImportRef string `json:"-"`
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Quantity int `json:"quantity"`
Insured bool `json:"insured"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
// Edges
Location LocationSummary `json:"location"`
Labels []LabelSummary `json:"labels"`
}
ItemOut struct {
ItemSummary
SerialNumber string `json:"serialNumber"`
ModelNumber string `json:"modelNumber"`
Manufacturer string `json:"manufacturer"`
// Warranty
LifetimeWarranty bool `json:"lifetimeWarranty"`
WarrantyExpires time.Time `json:"warrantyExpires"`
WarrantyDetails string `json:"warrantyDetails"`
// Purchase
PurchaseTime time.Time `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"`
PurchasePrice float64 `json:"purchasePrice,string"`
// Sold
SoldTime time.Time `json:"soldTime"`
SoldTo string `json:"soldTo"`
SoldPrice float64 `json:"soldPrice,string"`
SoldNotes string `json:"soldNotes"`
// Extras
Notes string `json:"notes"`
Attachments []ItemAttachment `json:"attachments"`
// Future
// Fields []*FieldSummary `json:"fields"`
}
)
var (
mapItemsSummaryErr = mapTEachErrFunc(mapItemSummary)
)
func mapItemSummary(item *ent.Item) ItemSummary {
var location LocationSummary
if item.Edges.Location != nil {
location = mapLocationSummary(item.Edges.Location)
}
var labels []LabelSummary
if item.Edges.Label != nil {
labels = mapEach(item.Edges.Label, mapLabelSummary)
}
return ItemSummary{
ID: item.ID,
Name: item.Name,
Description: item.Description,
Quantity: item.Quantity,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
// Edges
Location: location,
Labels: labels,
// Warranty
Insured: item.Insured,
}
}
var (
mapItemOutErr = mapTErrFunc(mapItemOut)
)
func mapItemOut(item *ent.Item) ItemOut {
var attachments []ItemAttachment
if item.Edges.Attachments != nil {
attachments = mapEach(item.Edges.Attachments, ToItemAttachment)
}
return ItemOut{
ItemSummary: mapItemSummary(item),
LifetimeWarranty: item.LifetimeWarranty,
WarrantyExpires: item.WarrantyExpires,
WarrantyDetails: item.WarrantyDetails,
// Identification
SerialNumber: item.SerialNumber,
ModelNumber: item.ModelNumber,
Manufacturer: item.Manufacturer,
// Purchase
PurchaseTime: item.PurchaseTime,
PurchaseFrom: item.PurchaseFrom,
PurchasePrice: item.PurchasePrice,
// Sold
SoldTime: item.SoldTime,
SoldTo: item.SoldTo,
SoldPrice: item.SoldPrice,
SoldNotes: item.SoldNotes,
// Extras
Notes: item.Notes,
Attachments: attachments,
}
}
func (e *ItemsRepository) getOne(ctx context.Context, where ...predicate.Item) (ItemOut, error) {
q := e.db.Item.Query().Where(where...)
return mapItemOutErr(q.
WithFields().
WithLabel().
WithLocation().
@ -24,19 +190,32 @@ func (e *ItemsRepository) GetOne(ctx context.Context, id uuid.UUID) (*ent.Item,
WithAttachments(func(aq *ent.AttachmentQuery) {
aq.WithDocument()
}).
Only(ctx)
Only(ctx),
)
}
// GetOne returns a single item by ID. If the item does not exist, an error is returned.
// See also: GetOneByGroup to ensure that the item belongs to a specific group.
func (e *ItemsRepository) GetOne(ctx context.Context, id uuid.UUID) (ItemOut, error) {
return e.getOne(ctx, item.ID(id))
}
// GetOneByGroup returns a single item by ID. If the item does not exist, an error is returned.
// GetOneByGroup ensures that the item belongs to a specific group.
func (e *ItemsRepository) GetOneByGroup(ctx context.Context, gid, id uuid.UUID) (ItemOut, error) {
return e.getOne(ctx, item.ID(id), item.HasGroupWith(group.ID(gid)))
}
// GetAll returns all the items in the database with the Labels and Locations eager loaded.
func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]*ent.Item, error) {
return e.db.Item.Query().
func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]ItemSummary, error) {
return mapItemsSummaryErr(e.db.Item.Query().
Where(item.HasGroupWith(group.ID(gid))).
WithLabel().
WithLocation().
All(ctx)
All(ctx))
}
func (e *ItemsRepository) Create(ctx context.Context, gid uuid.UUID, data types.ItemCreate) (*ent.Item, error) {
func (e *ItemsRepository) Create(ctx context.Context, gid uuid.UUID, data ItemCreate) (ItemOut, error) {
q := e.db.Item.Create().
SetName(data.Name).
SetDescription(data.Description).
@ -49,7 +228,7 @@ func (e *ItemsRepository) Create(ctx context.Context, gid uuid.UUID, data types.
result, err := q.Save(ctx)
if err != nil {
return nil, err
return ItemOut{}, err
}
return e.GetOne(ctx, result.ID)
@ -59,8 +238,18 @@ func (e *ItemsRepository) Delete(ctx context.Context, id uuid.UUID) error {
return e.db.Item.DeleteOneID(id).Exec(ctx)
}
func (e *ItemsRepository) Update(ctx context.Context, data types.ItemUpdate) (*ent.Item, error) {
q := e.db.Item.UpdateOneID(data.ID).
func (e *ItemsRepository) DeleteByGroup(ctx context.Context, gid, id uuid.UUID) error {
_, err := e.db.Item.
Delete().
Where(
item.ID(id),
item.HasGroupWith(group.ID(gid)),
).Exec(ctx)
return err
}
func (e *ItemsRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data ItemUpdate) (ItemOut, error) {
q := e.db.Item.Update().Where(item.ID(data.ID), item.HasGroupWith(group.ID(gid))).
SetName(data.Name).
SetDescription(data.Description).
SetLocationID(data.LocationID).
@ -83,13 +272,13 @@ func (e *ItemsRepository) Update(ctx context.Context, data types.ItemUpdate) (*e
currentLabels, err := e.db.Item.Query().Where(item.ID(data.ID)).QueryLabel().All(ctx)
if err != nil {
return nil, err
return ItemOut{}, err
}
set := EntitiesToIDSet(currentLabels)
set := newIDSet(currentLabels)
for _, l := range data.LabelIDs {
if set.Has(l) {
if set.Contains(l) {
set.Remove(l)
continue
}
@ -102,7 +291,7 @@ func (e *ItemsRepository) Update(ctx context.Context, data types.ItemUpdate) (*e
err = q.Exec(ctx)
if err != nil {
return nil, err
return ItemOut{}, err
}
return e.GetOne(ctx, data.ID)

View file

@ -6,25 +6,23 @@ import (
"time"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/stretchr/testify/assert"
)
func itemFactory() types.ItemCreate {
return types.ItemCreate{
func itemFactory() ItemCreate {
return ItemCreate{
Name: fk.Str(10),
Description: fk.Str(100),
}
}
func useItems(t *testing.T, len int) []*ent.Item {
func useItems(t *testing.T, len int) []ItemOut {
t.Helper()
location, err := tRepos.Locations.Create(context.Background(), tGroup.ID, locationFactory())
assert.NoError(t, err)
items := make([]*ent.Item, len)
items := make([]ItemOut, len)
for i := 0; i < len; i++ {
itm := itemFactory()
itm.LocationID = location.ID
@ -107,7 +105,7 @@ func TestItemsRepository_Create_Location(t *testing.T) {
foundItem, err := tRepos.Items.GetOne(context.Background(), result.ID)
assert.NoError(t, err)
assert.Equal(t, result.ID, foundItem.ID)
assert.Equal(t, location.ID, foundItem.Edges.Location.ID)
assert.Equal(t, location.ID, foundItem.Location.ID)
// Cleanup - Also deletes item
err = tRepos.Locations.Delete(context.Background(), location.ID)
@ -168,18 +166,18 @@ func TestItemsRepository_Update_Labels(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Apply all labels to entity
updateData := types.ItemUpdate{
updateData := ItemUpdate{
ID: entity.ID,
Name: entity.Name,
LocationID: entity.Edges.Location.ID,
LocationID: entity.Location.ID,
LabelIDs: tt.args.labelIds,
}
updated, err := tRepos.Items.Update(context.Background(), updateData)
updated, err := tRepos.Items.UpdateByGroup(context.Background(), tGroup.ID, updateData)
assert.NoError(t, err)
assert.Len(t, tt.want, len(updated.Edges.Label))
assert.Len(t, tt.want, len(updated.Labels))
for _, label := range updated.Edges.Label {
for _, label := range updated.Labels {
assert.Contains(t, tt.want, label.ID)
}
})
@ -192,10 +190,10 @@ func TestItemsRepository_Update(t *testing.T) {
entity := entities[0]
updateData := types.ItemUpdate{
updateData := ItemUpdate{
ID: entity.ID,
Name: entity.Name,
LocationID: entity.Edges.Location.ID,
LocationID: entity.Location.ID,
SerialNumber: fk.Str(10),
LabelIDs: nil,
ModelNumber: fk.Str(10),
@ -213,7 +211,7 @@ func TestItemsRepository_Update(t *testing.T) {
LifetimeWarranty: true,
}
updatedEntity, err := tRepos.Items.Update(context.Background(), updateData)
updatedEntity, err := tRepos.Items.UpdateByGroup(context.Background(), tGroup.ID, updateData)
assert.NoError(t, err)
got, err := tRepos.Items.GetOne(context.Background(), updatedEntity.ID)
@ -221,7 +219,7 @@ func TestItemsRepository_Update(t *testing.T) {
assert.Equal(t, updateData.ID, got.ID)
assert.Equal(t, updateData.Name, got.Name)
assert.Equal(t, updateData.LocationID, got.Edges.Location.ID)
assert.Equal(t, updateData.LocationID, got.Location.ID)
assert.Equal(t, updateData.SerialNumber, got.SerialNumber)
assert.Equal(t, updateData.ModelNumber, got.ModelNumber)
assert.Equal(t, updateData.Manufacturer, got.Manufacturer)

View file

@ -2,34 +2,94 @@ package repo
import (
"context"
"time"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/ent/group"
"github.com/hay-kot/homebox/backend/ent/label"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/ent/predicate"
)
type LabelRepository struct {
db *ent.Client
}
type (
LabelCreate struct {
Name string `json:"name"`
Description string `json:"description"`
Color string `json:"color"`
}
func (r *LabelRepository) Get(ctx context.Context, ID uuid.UUID) (*ent.Label, error) {
return r.db.Label.Query().
Where(label.ID(ID)).
LabelUpdate struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Color string `json:"color"`
}
LabelSummary struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
LabelOut struct {
LabelSummary
Items []ItemSummary `json:"items"`
}
)
func mapLabelSummary(label *ent.Label) LabelSummary {
return LabelSummary{
ID: label.ID,
Name: label.Name,
Description: label.Description,
CreatedAt: label.CreatedAt,
UpdatedAt: label.UpdatedAt,
}
}
var (
mapLabelOutErr = mapTErrFunc(mapLabelOut)
mapLabelsOut = mapTEachErrFunc(mapLabelSummary)
)
func mapLabelOut(label *ent.Label) LabelOut {
return LabelOut{
LabelSummary: mapLabelSummary(label),
Items: mapEach(label.Edges.Items, mapItemSummary),
}
}
func (r *LabelRepository) getOne(ctx context.Context, where ...predicate.Label) (LabelOut, error) {
return mapLabelOutErr(r.db.Label.Query().
Where(where...).
WithGroup().
WithItems().
Only(ctx)
Only(ctx),
)
}
func (r *LabelRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]*ent.Label, error) {
return r.db.Label.Query().
func (r *LabelRepository) GetOne(ctx context.Context, ID uuid.UUID) (LabelOut, error) {
return r.getOne(ctx, label.ID(ID))
}
func (r *LabelRepository) GetOneByGroup(ctx context.Context, gid, ld uuid.UUID) (LabelOut, error) {
return r.getOne(ctx, label.ID(ld), label.HasGroupWith(group.ID(gid)))
}
func (r *LabelRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]LabelSummary, error) {
return mapLabelsOut(r.db.Label.Query().
Where(label.HasGroupWith(group.ID(groupId))).
WithGroup().
All(ctx)
All(ctx),
)
}
func (r *LabelRepository) Create(ctx context.Context, groupdId uuid.UUID, data types.LabelCreate) (*ent.Label, error) {
func (r *LabelRepository) Create(ctx context.Context, groupdId uuid.UUID, data LabelCreate) (LabelOut, error) {
label, err := r.db.Label.Create().
SetName(data.Name).
SetDescription(data.Description).
@ -37,11 +97,15 @@ func (r *LabelRepository) Create(ctx context.Context, groupdId uuid.UUID, data t
SetGroupID(groupdId).
Save(ctx)
if err != nil {
return LabelOut{}, err
}
label.Edges.Group = &ent.Group{ID: groupdId} // bootstrap group ID
return label, err
return mapLabelOut(label), err
}
func (r *LabelRepository) Update(ctx context.Context, data types.LabelUpdate) (*ent.Label, error) {
func (r *LabelRepository) Update(ctx context.Context, data LabelUpdate) (LabelOut, error) {
_, err := r.db.Label.UpdateOneID(data.ID).
SetName(data.Name).
SetDescription(data.Description).
@ -49,10 +113,10 @@ func (r *LabelRepository) Update(ctx context.Context, data types.LabelUpdate) (*
Save(ctx)
if err != nil {
return nil, err
return LabelOut{}, err
}
return r.Get(ctx, data.ID)
return r.GetOne(ctx, data.ID)
}
func (r *LabelRepository) Delete(ctx context.Context, id uuid.UUID) error {

View file

@ -4,22 +4,20 @@ import (
"context"
"testing"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/stretchr/testify/assert"
)
func labelFactory() types.LabelCreate {
return types.LabelCreate{
func labelFactory() LabelCreate {
return LabelCreate{
Name: fk.Str(10),
Description: fk.Str(100),
}
}
func useLabels(t *testing.T, len int) []*ent.Label {
func useLabels(t *testing.T, len int) []LabelOut {
t.Helper()
labels := make([]*ent.Label, len)
labels := make([]LabelOut, len)
for i := 0; i < len; i++ {
itm := labelFactory()
@ -42,7 +40,7 @@ func TestLabelRepository_Get(t *testing.T) {
label := labels[0]
// Get by ID
foundLoc, err := tRepos.Labels.Get(context.Background(), label.ID)
foundLoc, err := tRepos.Labels.GetOne(context.Background(), label.ID)
assert.NoError(t, err)
assert.Equal(t, label.ID, foundLoc.ID)
}
@ -60,7 +58,7 @@ func TestLabelRepository_Create(t *testing.T) {
assert.NoError(t, err)
// Get by ID
foundLoc, err := tRepos.Labels.Get(context.Background(), loc.ID)
foundLoc, err := tRepos.Labels.GetOne(context.Background(), loc.ID)
assert.NoError(t, err)
assert.Equal(t, loc.ID, foundLoc.ID)
@ -72,7 +70,7 @@ func TestLabelRepository_Update(t *testing.T) {
loc, err := tRepos.Labels.Create(context.Background(), tGroup.ID, labelFactory())
assert.NoError(t, err)
updateData := types.LabelUpdate{
updateData := LabelUpdate{
ID: loc.ID,
Name: fk.Str(10),
Description: fk.Str(100),
@ -81,7 +79,7 @@ func TestLabelRepository_Update(t *testing.T) {
update, err := tRepos.Labels.Update(context.Background(), updateData)
assert.NoError(t, err)
foundLoc, err := tRepos.Labels.Get(context.Background(), loc.ID)
foundLoc, err := tRepos.Labels.GetOne(context.Background(), loc.ID)
assert.NoError(t, err)
assert.Equal(t, update.ID, foundLoc.ID)
@ -99,6 +97,6 @@ func TestLabelRepository_Delete(t *testing.T) {
err = tRepos.Labels.Delete(context.Background(), loc.ID)
assert.NoError(t, err)
_, err = tRepos.Labels.Get(context.Background(), loc.ID)
_, err = tRepos.Labels.GetOne(context.Background(), loc.ID)
assert.Error(t, err)
}

View file

@ -2,24 +2,79 @@ package repo
import (
"context"
"time"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/ent/group"
"github.com/hay-kot/homebox/backend/ent/location"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/ent/predicate"
)
type LocationRepository struct {
db *ent.Client
}
type LocationWithCount struct {
*ent.Location
ItemCount int `json:"itemCount"`
type (
LocationCreate struct {
Name string `json:"name"`
Description string `json:"description"`
}
LocationUpdate struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
LocationSummary struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
LocationOutCount struct {
LocationSummary
ItemCount int `json:"itemCount"`
}
LocationOut struct {
LocationSummary
Items []ItemSummary `json:"items"`
}
)
func mapLocationSummary(location *ent.Location) LocationSummary {
return LocationSummary{
ID: location.ID,
Name: location.Name,
Description: location.Description,
CreatedAt: location.CreatedAt,
UpdatedAt: location.UpdatedAt,
}
}
var (
mapLocationOutErr = mapTErrFunc(mapLocationOut)
)
func mapLocationOut(location *ent.Location) LocationOut {
return LocationOut{
LocationSummary: LocationSummary{
ID: location.ID,
Name: location.Name,
Description: location.Description,
CreatedAt: location.CreatedAt,
UpdatedAt: location.UpdatedAt,
},
Items: mapEach(location.Edges.Items, mapItemSummary),
}
}
// GetALlWithCount returns all locations with item count field populated
func (r *LocationRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]LocationWithCount, error) {
func (r *LocationRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]LocationOutCount, error) {
query := `--sql
SELECT
id,
@ -46,54 +101,62 @@ func (r *LocationRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]L
return nil, err
}
list := []LocationWithCount{}
list := []LocationOutCount{}
for rows.Next() {
var loc ent.Location
var ct LocationWithCount
err := rows.Scan(&loc.ID, &loc.Name, &loc.Description, &loc.CreatedAt, &loc.UpdatedAt, &ct.ItemCount)
var ct LocationOutCount
err := rows.Scan(&ct.ID, &ct.Name, &ct.Description, &ct.CreatedAt, &ct.UpdatedAt, &ct.ItemCount)
if err != nil {
return nil, err
}
ct.Location = &loc
list = append(list, ct)
}
return list, err
}
func (r *LocationRepository) Get(ctx context.Context, ID uuid.UUID) (*ent.Location, error) {
return r.db.Location.Query().
Where(location.ID(ID)).
func (r *LocationRepository) getOne(ctx context.Context, where ...predicate.Location) (LocationOut, error) {
return mapLocationOutErr(r.db.Location.Query().
Where(where...).
WithGroup().
WithItems(func(iq *ent.ItemQuery) {
iq.WithLabel()
}).
Only(ctx)
Only(ctx))
}
func (r *LocationRepository) Create(ctx context.Context, groupdId uuid.UUID, data types.LocationCreate) (*ent.Location, error) {
func (r *LocationRepository) Get(ctx context.Context, ID uuid.UUID) (LocationOut, error) {
return r.getOne(ctx, location.ID(ID))
}
func (r *LocationRepository) GetOneByGroup(ctx context.Context, GID, ID uuid.UUID) (LocationOut, error) {
return r.getOne(ctx, location.ID(ID), location.HasGroupWith(group.ID(GID)))
}
func (r *LocationRepository) Create(ctx context.Context, gid uuid.UUID, data LocationCreate) (LocationOut, error) {
location, err := r.db.Location.Create().
SetName(data.Name).
SetDescription(data.Description).
SetGroupID(groupdId).
SetGroupID(gid).
Save(ctx)
if err != nil {
return nil, err
return LocationOut{}, err
}
location.Edges.Group = &ent.Group{ID: groupdId} // bootstrap group ID
return location, err
location.Edges.Group = &ent.Group{ID: gid} // bootstrap group ID
return mapLocationOut(location), nil
}
func (r *LocationRepository) Update(ctx context.Context, data types.LocationUpdate) (*ent.Location, error) {
func (r *LocationRepository) Update(ctx context.Context, data LocationUpdate) (LocationOut, error) {
_, err := r.db.Location.UpdateOneID(data.ID).
SetName(data.Name).
SetDescription(data.Description).
Save(ctx)
if err != nil {
return nil, err
return LocationOut{}, err
}
return r.Get(ctx, data.ID)

View file

@ -4,12 +4,11 @@ import (
"context"
"testing"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/stretchr/testify/assert"
)
func locationFactory() types.LocationCreate {
return types.LocationCreate{
func locationFactory() LocationCreate {
return LocationCreate{
Name: fk.Str(10),
Description: fk.Str(100),
}
@ -30,13 +29,13 @@ func TestLocationRepository_Get(t *testing.T) {
func TestLocationRepositoryGetAllWithCount(t *testing.T) {
ctx := context.Background()
result, err := tRepos.Locations.Create(ctx, tGroup.ID, types.LocationCreate{
result, err := tRepos.Locations.Create(ctx, tGroup.ID, LocationCreate{
Name: fk.Str(10),
Description: fk.Str(100),
})
assert.NoError(t, err)
_, err = tRepos.Items.Create(ctx, tGroup.ID, types.ItemCreate{
_, err = tRepos.Items.Create(ctx, tGroup.ID, ItemCreate{
Name: fk.Str(10),
Description: fk.Str(100),
LocationID: result.ID,
@ -72,7 +71,7 @@ func TestLocationRepository_Update(t *testing.T) {
loc, err := tRepos.Locations.Create(context.Background(), tGroup.ID, locationFactory())
assert.NoError(t, err)
updateData := types.LocationUpdate{
updateData := LocationUpdate{
ID: loc.ID,
Name: fk.Str(10),
Description: fk.Str(100),

View file

@ -4,17 +4,34 @@ import (
"context"
"time"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/ent/authtokens"
"github.com/hay-kot/homebox/backend/internal/types"
)
type TokenRepository struct {
db *ent.Client
}
type (
UserAuthTokenCreate struct {
TokenHash []byte `json:"token"`
UserID uuid.UUID `json:"userId"`
ExpiresAt time.Time `json:"expiresAt"`
}
UserAuthToken struct {
UserAuthTokenCreate
CreatedAt time.Time `json:"createdAt"`
}
)
func (u UserAuthToken) IsExpired() bool {
return u.ExpiresAt.Before(time.Now())
}
// GetUserFromToken get's a user from a token
func (r *TokenRepository) GetUserFromToken(ctx context.Context, token []byte) (*ent.User, error) {
func (r *TokenRepository) GetUserFromToken(ctx context.Context, token []byte) (UserOut, error) {
user, err := r.db.AuthTokens.Query().
Where(authtokens.Token(token)).
Where(authtokens.ExpiresAtGTE(time.Now())).
@ -24,15 +41,14 @@ func (r *TokenRepository) GetUserFromToken(ctx context.Context, token []byte) (*
Only(ctx)
if err != nil {
return nil, err
return UserOut{}, err
}
return user, nil
return mapUserOut(user), nil
}
// Creates a token for a user
func (r *TokenRepository) CreateToken(ctx context.Context, createToken types.UserAuthTokenCreate) (types.UserAuthToken, error) {
tokenOut := types.UserAuthToken{}
func (r *TokenRepository) CreateToken(ctx context.Context, createToken UserAuthTokenCreate) (UserAuthToken, error) {
dbToken, err := r.db.AuthTokens.Create().
SetToken(createToken.TokenHash).
@ -41,15 +57,17 @@ func (r *TokenRepository) CreateToken(ctx context.Context, createToken types.Use
Save(ctx)
if err != nil {
return tokenOut, err
return UserAuthToken{}, err
}
tokenOut.TokenHash = dbToken.Token
tokenOut.UserID = createToken.UserID
tokenOut.CreatedAt = dbToken.CreatedAt
tokenOut.ExpiresAt = dbToken.ExpiresAt
return tokenOut, nil
return UserAuthToken{
UserAuthTokenCreate: UserAuthTokenCreate{
TokenHash: dbToken.Token,
UserID: createToken.UserID,
ExpiresAt: dbToken.ExpiresAt,
},
CreatedAt: dbToken.CreatedAt,
}, nil
}
// DeleteToken remove a single token from the database - equivalent to revoke or logout

View file

@ -5,7 +5,6 @@ import (
"testing"
"time"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/pkgs/hasher"
"github.com/stretchr/testify/assert"
)
@ -22,7 +21,7 @@ func TestAuthTokenRepo_CreateToken(t *testing.T) {
generatedToken := hasher.GenerateToken()
token, err := tRepos.AuthTokens.CreateToken(ctx, types.UserAuthTokenCreate{
token, err := tRepos.AuthTokens.CreateToken(ctx, UserAuthTokenCreate{
TokenHash: generatedToken.Hash,
ExpiresAt: expiresAt,
UserID: userOut.ID,
@ -50,7 +49,7 @@ func TestAuthTokenRepo_DeleteToken(t *testing.T) {
generatedToken := hasher.GenerateToken()
_, err = tRepos.AuthTokens.CreateToken(ctx, types.UserAuthTokenCreate{
_, err = tRepos.AuthTokens.CreateToken(ctx, UserAuthTokenCreate{
TokenHash: generatedToken.Hash,
ExpiresAt: expiresAt,
UserID: userOut.ID,
@ -72,7 +71,7 @@ func TestAuthTokenRepo_GetUserByToken(t *testing.T) {
expiresAt := time.Now().Add(time.Hour)
generatedToken := hasher.GenerateToken()
token, err := tRepos.AuthTokens.CreateToken(ctx, types.UserAuthTokenCreate{
token, err := tRepos.AuthTokens.CreateToken(ctx, UserAuthTokenCreate{
TokenHash: generatedToken.Hash,
ExpiresAt: expiresAt,
UserID: userOut.ID,
@ -101,13 +100,13 @@ func TestAuthTokenRepo_PurgeExpiredTokens(t *testing.T) {
user := userFactory()
userOut, _ := tRepos.Users.Create(ctx, user)
createdTokens := []types.UserAuthToken{}
createdTokens := []UserAuthToken{}
for i := 0; i < 5; i++ {
expiresAt := time.Now()
generatedToken := hasher.GenerateToken()
createdToken, err := tRepos.AuthTokens.CreateToken(ctx, types.UserAuthTokenCreate{
createdToken, err := tRepos.AuthTokens.CreateToken(ctx, UserAuthTokenCreate{
TokenHash: generatedToken.Hash,
ExpiresAt: expiresAt,
UserID: userOut.ID,

View file

@ -6,37 +6,77 @@ import (
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/ent/user"
"github.com/hay-kot/homebox/backend/internal/types"
)
type UserRepository struct {
db *ent.Client
}
func (e *UserRepository) GetOneId(ctx context.Context, id uuid.UUID) (*ent.User, error) {
return e.db.User.Query().
Where(user.ID(id)).
WithGroup().
Only(ctx)
}
func (e *UserRepository) GetOneEmail(ctx context.Context, email string) (*ent.User, error) {
return e.db.User.Query().
Where(user.Email(email)).
WithGroup().
Only(ctx)
}
func (e *UserRepository) GetAll(ctx context.Context) ([]*ent.User, error) {
return e.db.User.Query().WithGroup().All(ctx)
}
func (e *UserRepository) Create(ctx context.Context, usr types.UserCreate) (*ent.User, error) {
err := usr.Validate()
if err != nil {
return &ent.User{}, err
type (
// UserCreate is the Data object contain the requirements of creating a user
// in the database. It should to create users from an API unless the user has
// rights to create SuperUsers. For regular user in data use the UserIn struct.
UserCreate struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
IsSuperuser bool `json:"isSuperuser"`
GroupID uuid.UUID `json:"groupID"`
}
UserUpdate struct {
Name string `json:"name"`
Email string `json:"email"`
}
UserOut struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
IsSuperuser bool `json:"isSuperuser"`
GroupID uuid.UUID `json:"groupId"`
GroupName string `json:"groupName"`
PasswordHash string `json:"-"`
}
)
var (
mapUserOutErr = mapTErrFunc(mapUserOut)
mapUsersOutErr = mapTEachErrFunc(mapUserOut)
)
func mapUserOut(user *ent.User) UserOut {
return UserOut{
ID: user.ID,
Name: user.Name,
Email: user.Email,
IsSuperuser: user.IsSuperuser,
GroupID: user.Edges.Group.ID,
GroupName: user.Edges.Group.Name,
PasswordHash: user.Password,
}
}
func (e *UserRepository) GetOneId(ctx context.Context, id uuid.UUID) (UserOut, error) {
return mapUserOutErr(e.db.User.Query().
Where(user.ID(id)).
WithGroup().
Only(ctx))
}
func (e *UserRepository) GetOneEmail(ctx context.Context, email string) (UserOut, error) {
return mapUserOutErr(e.db.User.Query().
Where(user.Email(email)).
WithGroup().
Only(ctx),
)
}
func (e *UserRepository) GetAll(ctx context.Context) ([]UserOut, error) {
return mapUsersOutErr(e.db.User.Query().WithGroup().All(ctx))
}
func (e *UserRepository) Create(ctx context.Context, usr UserCreate) (UserOut, error) {
entUser, err := e.db.User.
Create().
SetName(usr.Name).
@ -46,13 +86,13 @@ func (e *UserRepository) Create(ctx context.Context, usr types.UserCreate) (*ent
SetGroupID(usr.GroupID).
Save(ctx)
if err != nil {
return entUser, err
return UserOut{}, err
}
return e.GetOneId(ctx, entUser.ID)
}
func (e *UserRepository) Update(ctx context.Context, ID uuid.UUID, data types.UserUpdate) error {
func (e *UserRepository) Update(ctx context.Context, ID uuid.UUID, data UserUpdate) error {
q := e.db.User.Update().
Where(user.ID(ID)).
SetName(data.Name).

View file

@ -5,14 +5,11 @@ import (
"fmt"
"testing"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/stretchr/testify/assert"
)
func userFactory() types.UserCreate {
return types.UserCreate{
func userFactory() UserCreate {
return UserCreate{
Name: fk.Str(10),
Email: fk.Email(),
Password: fk.Str(10),
@ -61,7 +58,7 @@ func TestUserRepo_GetOneId(t *testing.T) {
func TestUserRepo_GetAll(t *testing.T) {
// Setup
toCreate := []types.UserCreate{
toCreate := []UserCreate{
userFactory(),
userFactory(),
userFactory(),
@ -70,7 +67,7 @@ func TestUserRepo_GetAll(t *testing.T) {
ctx := context.Background()
created := []*ent.User{}
created := []UserOut{}
for _, usr := range toCreate {
usrOut, _ := tRepos.Users.Create(ctx, usr)
@ -90,7 +87,7 @@ func TestUserRepo_GetAll(t *testing.T) {
assert.Equal(t, usr.Email, usr2.Email)
// Check groups are loaded
assert.NotNil(t, usr2.Edges.Group)
assert.NotNil(t, usr2.GroupID)
}
}
}
@ -108,7 +105,7 @@ func TestUserRepo_Update(t *testing.T) {
user, err := tRepos.Users.Create(context.Background(), userFactory())
assert.NoError(t, err)
updateData := types.UserUpdate{
updateData := UserUpdate{
Name: fk.Str(10),
Email: fk.Email(),
}

View file

@ -15,7 +15,7 @@ type AllRepos struct {
Attachments *AttachmentRepo
}
func EntAllRepos(db *ent.Client) *AllRepos {
func EntAllRepos(db *ent.Client, root string) *AllRepos {
return &AllRepos{
Users: &UserRepository{db},
AuthTokens: &TokenRepository{db},
@ -23,7 +23,7 @@ func EntAllRepos(db *ent.Client) *AllRepos {
Locations: &LocationRepository{db},
Labels: &LabelRepository{db},
Items: &ItemsRepository{db},
Docs: &DocumentRepository{db},
Docs: &DocumentRepository{db, root},
DocTokens: &DocumentTokensRepository{db},
Attachments: &AttachmentRepo{db},
}

View file

@ -4,29 +4,23 @@ import "github.com/hay-kot/homebox/backend/internal/repo"
type AllServices struct {
User *UserService
Admin *AdminService
Location *LocationService
Labels *LabelService
Items *ItemService
}
func NewServices(repos *repo.AllRepos, root string) *AllServices {
func NewServices(repos *repo.AllRepos) *AllServices {
if repos == nil {
panic("repos cannot be nil")
}
if root == "" {
panic("root cannot be empty")
}
return &AllServices{
User: &UserService{repos},
Admin: &AdminService{repos},
Location: &LocationService{repos},
Labels: &LabelService{repos},
Items: &ItemService{
repo: repos,
filepath: root,
at: attachmentTokens{},
repo: repos,
at: attachmentTokens{},
},
}
}

View file

@ -4,7 +4,7 @@ import (
"context"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/internal/repo"
)
type contextKeys struct {
@ -26,7 +26,7 @@ type Context struct {
GID uuid.UUID
// User is the acting user.
User *types.UserOut
User *repo.UserOut
}
// NewContext is a helper function that returns the service context from the context.
@ -43,16 +43,16 @@ func NewContext(ctx context.Context) Context {
// SetUserCtx is a helper function that sets the ContextUser and ContextUserToken
// values within the context of a web request (or any context).
func SetUserCtx(ctx context.Context, user *types.UserOut, token string) context.Context {
func SetUserCtx(ctx context.Context, user *repo.UserOut, token string) context.Context {
ctx = context.WithValue(ctx, ContextUser, user)
ctx = context.WithValue(ctx, ContextUserToken, token)
return ctx
}
// UseUserCtx is a helper function that returns the user from the context.
func UseUserCtx(ctx context.Context) *types.UserOut {
func UseUserCtx(ctx context.Context) *repo.UserOut {
if val := ctx.Value(ContextUser); val != nil {
return val.(*types.UserOut)
return val.(*repo.UserOut)
}
return nil
}

View file

@ -5,12 +5,12 @@ import (
"testing"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/stretchr/testify/assert"
)
func Test_SetAuthContext(t *testing.T) {
user := &types.UserOut{
user := &repo.UserOut{
ID: uuid.New(),
}

View file

@ -10,7 +10,6 @@ import (
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/pkgs/faker"
_ "github.com/mattn/go-sqlite3"
)
@ -21,7 +20,7 @@ var (
tCtx = Context{}
tClient *ent.Client
tRepos *repo.AllRepos
tUser *ent.User
tUser repo.UserOut
tGroup *ent.Group
tSvc *AllServices
)
@ -37,7 +36,7 @@ func bootstrap() {
log.Fatal(err)
}
tUser, err = tRepos.Users.Create(ctx, types.UserCreate{
tUser, err = tRepos.Users.Create(ctx, repo.UserCreate{
Name: fk.Str(10),
Email: fk.Email(),
Password: fk.Str(10),
@ -63,11 +62,10 @@ func TestMain(m *testing.M) {
}
tClient = client
tRepos = repo.EntAllRepos(tClient)
tSvc = NewServices(tRepos, "/tmp/homebox")
tRepos = repo.EntAllRepos(tClient, os.TempDir()+"/homebox")
tSvc = NewServices(tRepos)
defer client.Close()
bootstrap()
tCtx = Context{
Context: context.Background(),

View file

@ -1,9 +0,0 @@
package mappers
func MapEach[T any, U any](items []T, fn func(T) U) []U {
result := make([]U, len(items))
for i, item := range items {
result[i] = fn(item)
}
return result
}

View file

@ -1,91 +0,0 @@
package mappers
import (
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/internal/types"
)
func ToItemAttachment(attachment *ent.Attachment) *types.ItemAttachment {
return &types.ItemAttachment{
ID: attachment.ID,
CreatedAt: attachment.CreatedAt,
UpdatedAt: attachment.UpdatedAt,
Type: attachment.Type.String(),
Document: types.DocumentOut{
ID: attachment.Edges.Document.ID,
Title: attachment.Edges.Document.Title,
Path: attachment.Edges.Document.Path,
},
}
}
func ToItemSummary(item *ent.Item) *types.ItemSummary {
var location *types.LocationSummary
if item.Edges.Location != nil {
location = ToLocationSummary(item.Edges.Location)
}
var labels []*types.LabelSummary
if item.Edges.Label != nil {
labels = MapEach(item.Edges.Label, ToLabelSummary)
}
return &types.ItemSummary{
ID: item.ID,
Name: item.Name,
Description: item.Description,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
Quantity: item.Quantity,
Insured: item.Insured,
// Warranty
LifetimeWarranty: item.LifetimeWarranty,
WarrantyExpires: item.WarrantyExpires,
WarrantyDetails: item.WarrantyDetails,
// Edges
Location: location,
Labels: labels,
// Identification
SerialNumber: item.SerialNumber,
ModelNumber: item.ModelNumber,
Manufacturer: item.Manufacturer,
// Purchase
PurchaseTime: item.PurchaseTime,
PurchaseFrom: item.PurchaseFrom,
PurchasePrice: item.PurchasePrice,
// Sold
SoldTime: item.SoldTime,
SoldTo: item.SoldTo,
SoldPrice: item.SoldPrice,
SoldNotes: item.SoldNotes,
// Extras
Notes: item.Notes,
}
}
func ToItemSummaryErr(item *ent.Item, err error) (*types.ItemSummary, error) {
return ToItemSummary(item), err
}
func ToItemOut(item *ent.Item) *types.ItemOut {
var attachments []*types.ItemAttachment
if item.Edges.Attachments != nil {
attachments = MapEach(item.Edges.Attachments, ToItemAttachment)
}
return &types.ItemOut{
ItemSummary: *ToItemSummary(item),
Attachments: attachments,
}
}
func ToItemOutErr(item *ent.Item, err error) (*types.ItemOut, error) {
return ToItemOut(item), err
}

View file

@ -1,31 +0,0 @@
package mappers
import (
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/internal/types"
)
func ToLabelSummary(label *ent.Label) *types.LabelSummary {
return &types.LabelSummary{
ID: label.ID,
Name: label.Name,
Description: label.Description,
CreatedAt: label.CreatedAt,
UpdatedAt: label.UpdatedAt,
}
}
func ToLabelSummaryErr(label *ent.Label, err error) (*types.LabelSummary, error) {
return ToLabelSummary(label), err
}
func ToLabelOut(label *ent.Label) *types.LabelOut {
return &types.LabelOut{
LabelSummary: *ToLabelSummary(label),
Items: MapEach(label.Edges.Items, ToItemSummary),
}
}
func ToLabelOutErr(label *ent.Label, err error) (*types.LabelOut, error) {
return ToLabelOut(label), err
}

View file

@ -1,55 +0,0 @@
package mappers
import (
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/internal/types"
)
func ToLocationCount(location *repo.LocationWithCount) *types.LocationCount {
return &types.LocationCount{
LocationSummary: types.LocationSummary{
ID: location.ID,
Name: location.Name,
Description: location.Description,
CreatedAt: location.CreatedAt,
UpdatedAt: location.UpdatedAt,
},
ItemCount: location.ItemCount,
}
}
func ToLocationCountErr(location *repo.LocationWithCount, err error) (*types.LocationCount, error) {
return ToLocationCount(location), err
}
func ToLocationSummary(location *ent.Location) *types.LocationSummary {
return &types.LocationSummary{
ID: location.ID,
Name: location.Name,
Description: location.Description,
CreatedAt: location.CreatedAt,
UpdatedAt: location.UpdatedAt,
}
}
func ToLocationSummaryErr(location *ent.Location, err error) (*types.LocationSummary, error) {
return ToLocationSummary(location), err
}
func ToLocationOut(location *ent.Location) *types.LocationOut {
return &types.LocationOut{
LocationSummary: types.LocationSummary{
ID: location.ID,
Name: location.Name,
Description: location.Description,
CreatedAt: location.CreatedAt,
UpdatedAt: location.UpdatedAt,
},
Items: MapEach(location.Edges.Items, ToItemSummary),
}
}
func ToLocationOutErr(location *ent.Location, err error) (*types.LocationOut, error) {
return ToLocationOut(location), err
}

View file

@ -1,20 +0,0 @@
package mappers
import (
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/internal/types"
)
func ToOutUser(user *ent.User, err error) (*types.UserOut, error) {
if err != nil {
return &types.UserOut{}, err
}
return &types.UserOut{
ID: user.ID,
Name: user.Name,
Email: user.Email,
IsSuperuser: user.IsSuperuser,
GroupName: user.Edges.Group.Name,
GroupID: user.Edges.Group.ID,
}, nil
}

View file

@ -1,48 +0,0 @@
package services
import (
"context"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/internal/types"
)
type AdminService struct {
repos *repo.AllRepos
}
func (svc *AdminService) Create(ctx context.Context, usr types.UserCreate) (*ent.User, error) {
return svc.repos.Users.Create(ctx, usr)
}
func (svc *AdminService) GetAll(ctx context.Context) ([]*ent.User, error) {
return svc.repos.Users.GetAll(ctx)
}
func (svc *AdminService) GetByID(ctx context.Context, id uuid.UUID) (*ent.User, error) {
return svc.repos.Users.GetOneId(ctx, id)
}
func (svc *AdminService) GetByEmail(ctx context.Context, email string) (*ent.User, error) {
return svc.repos.Users.GetOneEmail(ctx, email)
}
func (svc *AdminService) UpdateProperties(ctx context.Context, ID uuid.UUID, data types.UserUpdate) (*ent.User, error) {
err := svc.repos.Users.Update(ctx, ID, data)
if err != nil {
return &ent.User{}, err
}
return svc.repos.Users.GetOneId(ctx, ID)
}
func (svc *AdminService) Delete(ctx context.Context, id uuid.UUID) error {
return svc.repos.Users.Delete(ctx, id)
}
func (svc *AdminService) DeleteAll(ctx context.Context) error {
return svc.repos.Users.DeleteAll(ctx)
}

View file

@ -7,8 +7,6 @@ import (
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/internal/services/mappers"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/rs/zerolog/log"
)
@ -26,76 +24,24 @@ type ItemService struct {
at attachmentTokens
}
func (svc *ItemService) GetOne(ctx context.Context, gid uuid.UUID, id uuid.UUID) (*types.ItemOut, error) {
result, err := svc.repo.Items.GetOne(ctx, id)
if err != nil {
return nil, err
}
if result.Edges.Group.ID != gid {
return nil, ErrNotOwner
}
return mappers.ToItemOut(result), nil
func (svc *ItemService) GetOne(ctx context.Context, gid uuid.UUID, id uuid.UUID) (repo.ItemOut, error) {
return svc.repo.Items.GetOneByGroup(ctx, gid, id)
}
func (svc *ItemService) GetAll(ctx context.Context, gid uuid.UUID) ([]*types.ItemSummary, error) {
items, err := svc.repo.Items.GetAll(ctx, gid)
if err != nil {
return nil, err
}
itemsOut := make([]*types.ItemSummary, len(items))
for i, item := range items {
itemsOut[i] = mappers.ToItemSummary(item)
}
return itemsOut, nil
func (svc *ItemService) GetAll(ctx context.Context, gid uuid.UUID) ([]repo.ItemSummary, error) {
return svc.repo.Items.GetAll(ctx, gid)
}
func (svc *ItemService) Create(ctx context.Context, gid uuid.UUID, data types.ItemCreate) (*types.ItemOut, error) {
item, err := svc.repo.Items.Create(ctx, gid, data)
if err != nil {
return nil, err
}
return mappers.ToItemOut(item), nil
func (svc *ItemService) Create(ctx context.Context, gid uuid.UUID, data repo.ItemCreate) (repo.ItemOut, error) {
return svc.repo.Items.Create(ctx, gid, data)
}
func (svc *ItemService) Delete(ctx context.Context, gid uuid.UUID, id uuid.UUID) error {
item, err := svc.repo.Items.GetOne(ctx, id)
if err != nil {
return err
}
if item.Edges.Group.ID != gid {
return ErrNotOwner
}
err = svc.repo.Items.Delete(ctx, id)
if err != nil {
return err
}
return nil
return svc.repo.Items.DeleteByGroup(ctx, gid, id)
}
func (svc *ItemService) Update(ctx context.Context, gid uuid.UUID, data types.ItemUpdate) (*types.ItemOut, error) {
item, err := svc.repo.Items.GetOne(ctx, data.ID)
if err != nil {
return nil, err
}
if item.Edges.Group.ID != gid {
return nil, ErrNotOwner
}
item, err = svc.repo.Items.Update(ctx, data)
if err != nil {
return nil, err
}
return mappers.ToItemOut(item), nil
func (svc *ItemService) Update(ctx context.Context, gid uuid.UUID, data repo.ItemUpdate) (repo.ItemOut, error) {
return svc.repo.Items.UpdateByGroup(ctx, gid, data)
}
func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]string) error {
@ -144,7 +90,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
fmt.Println("Creating Location: ", row.Location)
result, err := svc.repo.Locations.Create(ctx, gid, types.LocationCreate{
result, err := svc.repo.Locations.Create(ctx, gid, repo.LocationCreate{
Name: row.Location,
Description: "",
})
@ -159,7 +105,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
if _, ok := labels[label]; ok {
continue
}
result, err := svc.repo.Labels.Create(ctx, gid, types.LabelCreate{
result, err := svc.repo.Labels.Create(ctx, gid, repo.LabelCreate{
Name: label,
Description: "",
})
@ -185,7 +131,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
Str("locationId", locationID.String()).
Msgf("Creating Item: %s", row.Item.Name)
result, err := svc.repo.Items.Create(ctx, gid, types.ItemCreate{
result, err := svc.repo.Items.Create(ctx, gid, repo.ItemCreate{
ImportRef: row.Item.ImportRef,
Name: row.Item.Name,
Description: row.Item.Description,
@ -198,7 +144,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
}
// Update the item with the rest of the data
_, err = svc.repo.Items.Update(ctx, types.ItemUpdate{
_, err = svc.repo.Items.UpdateByGroup(ctx, gid, repo.ItemUpdate{
// Edges
LocationID: locationID,
LabelIDs: labelIDs,
@ -209,6 +155,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
Description: result.Description,
Insured: row.Item.Insured,
Notes: row.Item.Notes,
Quantity: row.Item.Quantity,
// Identifies the item as imported
SerialNumber: row.Item.SerialNumber,

View file

@ -4,14 +4,13 @@ import (
"context"
"io"
"os"
"path/filepath"
"time"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/ent"
"github.com/hay-kot/homebox/backend/ent/attachment"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/pkgs/hasher"
"github.com/hay-kot/homebox/backend/pkgs/pathlib"
"github.com/rs/zerolog/log"
)
@ -41,13 +40,10 @@ func (at attachmentTokens) Delete(token string) {
}
func (svc *ItemService) AttachmentToken(ctx Context, itemId, attachmentId uuid.UUID) (string, error) {
item, err := svc.repo.Items.GetOne(ctx, itemId)
_, err := svc.repo.Items.GetOneByGroup(ctx, ctx.GID, itemId)
if err != nil {
return "", err
}
if item.Edges.Group.ID != ctx.GID {
return "", ErrNotOwner
}
token := hasher.GenerateToken()
@ -67,52 +63,32 @@ func (svc *ItemService) AttachmentToken(ctx Context, itemId, attachmentId uuid.U
return token.Raw, nil
}
func (svc *ItemService) attachmentPath(gid, itemId uuid.UUID, filename string) string {
path := filepath.Join(svc.filepath, gid.String(), itemId.String(), filename)
path = pathlib.Safe(path)
log.Debug().Str("path", path).Msg("attachment path")
return path
}
func (svc *ItemService) AttachmentPath(ctx context.Context, token string) (string, error) {
func (svc *ItemService) AttachmentPath(ctx context.Context, token string) (*ent.Document, error) {
attachmentId, ok := svc.at.Get(token)
if !ok {
return "", ErrNotFound
return nil, ErrNotFound
}
attachment, err := svc.repo.Attachments.Get(ctx, attachmentId)
if err != nil {
return "", err
}
return attachment.Edges.Document.Path, nil
}
func (svc *ItemService) AttachmentUpdate(ctx Context, itemId uuid.UUID, data *types.ItemAttachmentUpdate) (*types.ItemOut, error) {
// Update Properties
attachment, err := svc.repo.Attachments.Update(ctx, data.ID, attachment.Type(data.Type))
if err != nil {
return nil, err
}
return attachment.Edges.Document, nil
}
func (svc *ItemService) AttachmentUpdate(ctx Context, itemId uuid.UUID, data *repo.ItemAttachmentUpdate) (repo.ItemOut, error) {
// Update Attachment
attachment, err := svc.repo.Attachments.Update(ctx, data.ID, attachment.Type(data.Type))
if err != nil {
return repo.ItemOut{}, err
}
// Update Document
attDoc := attachment.Edges.Document
if data.Title != attachment.Edges.Document.Title {
newPath := pathlib.Safe(svc.attachmentPath(ctx.GID, itemId, data.Title))
// Move File
err = os.Rename(attachment.Edges.Document.Path, newPath)
if err != nil {
return nil, err
}
_, err = svc.repo.Docs.Update(ctx, attDoc.ID, types.DocumentUpdate{
Title: data.Title,
Path: newPath,
})
if err != nil {
return nil, err
}
_, err = svc.repo.Docs.Rename(ctx, attDoc.ID, data.Title)
if err != nil {
return repo.ItemOut{}, err
}
return svc.GetOne(ctx, ctx.GID, itemId)
@ -121,50 +97,25 @@ func (svc *ItemService) AttachmentUpdate(ctx Context, itemId uuid.UUID, data *ty
// AttachmentAdd adds an attachment to an item by creating an entry in the Documents table and linking it to the Attachment
// Table and Items table. The file provided via the reader is stored on the file system based on the provided
// relative path during construction of the service.
func (svc *ItemService) AttachmentAdd(ctx Context, itemId uuid.UUID, filename string, attachmentType attachment.Type, file io.Reader) (*types.ItemOut, error) {
func (svc *ItemService) AttachmentAdd(ctx Context, itemId uuid.UUID, filename string, attachmentType attachment.Type, file io.Reader) (repo.ItemOut, error) {
// Get the Item
item, err := svc.repo.Items.GetOne(ctx, itemId)
_, err := svc.repo.Items.GetOneByGroup(ctx, ctx.GID, itemId)
if err != nil {
return nil, err
return repo.ItemOut{}, err
}
if item.Edges.Group.ID != ctx.GID {
return nil, ErrNotOwner
}
fp := svc.attachmentPath(ctx.GID, itemId, filename)
filename = filepath.Base(fp)
// Create the document
doc, err := svc.repo.Docs.Create(ctx, ctx.GID, types.DocumentCreate{
Title: filename,
Path: fp,
})
doc, err := svc.repo.Docs.Create(ctx, ctx.GID, repo.DocumentCreate{Title: filename, Content: file})
if err != nil {
return nil, err
log.Err(err).Msg("failed to create document")
return repo.ItemOut{}, err
}
// Create the attachment
_, err = svc.repo.Attachments.Create(ctx, itemId, doc.ID, attachmentType)
if err != nil {
return nil, err
}
// Read the contents and write them to a file on the file system
err = os.MkdirAll(filepath.Dir(doc.Path), os.ModePerm)
if err != nil {
return nil, err
}
f, err := os.Create(doc.Path)
if err != nil {
log.Err(err).Msg("failed to create file")
return nil, err
}
_, err = io.Copy(f, file)
if err != nil {
return nil, err
log.Err(err).Msg("failed to create attachment")
return repo.ItemOut{}, err
}
return svc.GetOne(ctx, ctx.GID, itemId)
@ -172,15 +123,11 @@ func (svc *ItemService) AttachmentAdd(ctx Context, itemId uuid.UUID, filename st
func (svc *ItemService) AttachmentDelete(ctx context.Context, gid, itemId, attachmentId uuid.UUID) error {
// Get the Item
item, err := svc.repo.Items.GetOne(ctx, itemId)
_, err := svc.repo.Items.GetOneByGroup(ctx, gid, itemId)
if err != nil {
return err
}
if item.Edges.Group.ID != gid {
return ErrNotOwner
}
attachment, err := svc.repo.Attachments.Get(ctx, attachmentId)
if err != nil {
return err

View file

@ -7,7 +7,7 @@ import (
"strings"
"testing"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/stretchr/testify/assert"
)
@ -19,14 +19,14 @@ func TestItemService_AddAttachment(t *testing.T) {
filepath: temp,
}
loc, err := tSvc.Location.Create(context.Background(), tGroup.ID, types.LocationCreate{
loc, err := tSvc.Location.Create(context.Background(), tGroup.ID, repo.LocationCreate{
Description: "test",
Name: "test",
})
assert.NoError(t, err)
assert.NotNil(t, loc)
itmC := types.ItemCreate{
itmC := repo.ItemCreate{
Name: fk.Str(10),
Description: fk.Str(10),
LocationID: loc.ID,
@ -52,7 +52,7 @@ func TestItemService_AddAttachment(t *testing.T) {
storedPath := afterAttachment.Attachments[0].Document.Path
// {root}/{group}/{item}/{attachment}
assert.Equal(t, path.Join(temp, tGroup.ID.String(), itm.ID.String(), "testfile.txt"), storedPath)
assert.Equal(t, path.Join(temp, "homebox", tGroup.ID.String(), "documents"), path.Dir(storedPath))
// Check that the file contents are correct
bts, err := os.ReadFile(storedPath)

View file

@ -6,7 +6,7 @@ import (
"strings"
"time"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/internal/repo"
)
var ErrInvalidCsv = errors.New("invalid csv")
@ -45,7 +45,7 @@ func parseInt(s string) int {
}
type csvRow struct {
Item types.ItemSummary
Item repo.ItemOut
Location string
LabelStr string
}
@ -54,12 +54,14 @@ func newCsvRow(row []string) csvRow {
return csvRow{
Location: row[1],
LabelStr: row[2],
Item: types.ItemSummary{
ImportRef: row[0],
Quantity: parseInt(row[3]),
Name: row[4],
Description: row[5],
Insured: parseBool(row[6]),
Item: repo.ItemOut{
ItemSummary: repo.ItemSummary{
ImportRef: row[0],
Quantity: parseInt(row[3]),
Name: row[4],
Description: row[5],
Insured: parseBool(row[6]),
},
SerialNumber: row[7],
ModelNumber: row[8],
Manufacturer: row[9],

View file

@ -71,19 +71,8 @@ func TestItemService_CsvImport(t *testing.T) {
for _, csvRow := range dataCsv {
if csvRow.Item.Name == item.Name {
assert.Equal(t, csvRow.Item.Description, item.Description)
assert.Equal(t, csvRow.Item.SerialNumber, item.SerialNumber)
assert.Equal(t, csvRow.Item.Manufacturer, item.Manufacturer)
assert.Equal(t, csvRow.Item.Notes, item.Notes)
// Purchase Fields
assert.Equal(t, csvRow.Item.PurchaseTime, item.PurchaseTime)
assert.Equal(t, csvRow.Item.PurchaseFrom, item.PurchaseFrom)
assert.Equal(t, csvRow.Item.PurchasePrice, item.PurchasePrice)
// Sold Fields
assert.Equal(t, csvRow.Item.SoldTime, item.SoldTime)
assert.Equal(t, csvRow.Item.SoldTo, item.SoldTo)
assert.Equal(t, csvRow.Item.SoldPrice, item.SoldPrice)
assert.Equal(t, csvRow.Item.Quantity, item.Quantity)
assert.Equal(t, csvRow.Item.Insured, item.Insured)
}
}
}

View file

@ -5,59 +5,33 @@ import (
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/internal/services/mappers"
"github.com/hay-kot/homebox/backend/internal/types"
)
type LabelService struct {
repos *repo.AllRepos
}
func (svc *LabelService) Create(ctx context.Context, groupId uuid.UUID, data types.LabelCreate) (*types.LabelSummary, error) {
label, err := svc.repos.Labels.Create(ctx, groupId, data)
return mappers.ToLabelSummaryErr(label, err)
func (svc *LabelService) Create(ctx context.Context, groupId uuid.UUID, data repo.LabelCreate) (repo.LabelOut, error) {
return svc.repos.Labels.Create(ctx, groupId, data)
}
func (svc *LabelService) Update(ctx context.Context, groupId uuid.UUID, data types.LabelUpdate) (*types.LabelSummary, error) {
label, err := svc.repos.Labels.Update(ctx, data)
return mappers.ToLabelSummaryErr(label, err)
func (svc *LabelService) Update(ctx context.Context, groupId uuid.UUID, data repo.LabelUpdate) (repo.LabelOut, error) {
return svc.repos.Labels.Update(ctx, data)
}
func (svc *LabelService) Delete(ctx context.Context, groupId uuid.UUID, id uuid.UUID) error {
label, err := svc.repos.Labels.Get(ctx, id)
func (svc *LabelService) Delete(ctx context.Context, gid uuid.UUID, id uuid.UUID) error {
_, err := svc.repos.Labels.GetOneByGroup(ctx, gid, id)
if err != nil {
return err
}
if label.Edges.Group.ID != groupId {
return ErrNotOwner
}
return svc.repos.Labels.Delete(ctx, id)
}
func (svc *LabelService) Get(ctx context.Context, groupId uuid.UUID, id uuid.UUID) (*types.LabelOut, error) {
label, err := svc.repos.Labels.Get(ctx, id)
func (svc *LabelService) Get(ctx context.Context, gid uuid.UUID, id uuid.UUID) (repo.LabelOut, error) {
return svc.repos.Labels.GetOneByGroup(ctx, gid, id)
if err != nil {
return nil, err
}
if label.Edges.Group.ID != groupId {
return nil, ErrNotOwner
}
return mappers.ToLabelOut(label), nil
}
func (svc *LabelService) GetAll(ctx context.Context, groupId uuid.UUID) ([]*types.LabelSummary, error) {
labels, err := svc.repos.Labels.GetAll(ctx, groupId)
if err != nil {
return nil, err
}
labelsOut := make([]*types.LabelSummary, len(labels))
for i, label := range labels {
labelsOut[i] = mappers.ToLabelSummary(label)
}
return labelsOut, nil
func (svc *LabelService) GetAll(ctx context.Context, groupId uuid.UUID) ([]repo.LabelSummary, error) {
return svc.repos.Labels.GetAll(ctx, groupId)
}

View file

@ -6,8 +6,6 @@ import (
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/internal/services/mappers"
"github.com/hay-kot/homebox/backend/internal/types"
)
var (
@ -18,59 +16,32 @@ type LocationService struct {
repos *repo.AllRepos
}
func (svc *LocationService) GetOne(ctx context.Context, groupId uuid.UUID, id uuid.UUID) (*types.LocationOut, error) {
location, err := svc.repos.Locations.Get(ctx, id)
if err != nil {
return nil, err
}
if location.Edges.Group.ID != groupId {
return nil, ErrNotOwner
}
return mappers.ToLocationOut(location), nil
func (svc *LocationService) GetOne(ctx context.Context, groupId uuid.UUID, id uuid.UUID) (repo.LocationOut, error) {
return svc.repos.Locations.GetOneByGroup(ctx, groupId, id)
}
func (svc *LocationService) GetAll(ctx context.Context, groupId uuid.UUID) ([]*types.LocationCount, error) {
locations, err := svc.repos.Locations.GetAll(ctx, groupId)
if err != nil {
return nil, err
}
locationsOut := make([]*types.LocationCount, len(locations))
for i, location := range locations {
locationsOut[i] = mappers.ToLocationCount(&location)
}
return locationsOut, nil
func (svc *LocationService) GetAll(ctx context.Context, groupId uuid.UUID) ([]repo.LocationOutCount, error) {
return svc.repos.Locations.GetAll(ctx, groupId)
}
func (svc *LocationService) Create(ctx context.Context, groupId uuid.UUID, data types.LocationCreate) (*types.LocationOut, error) {
location, err := svc.repos.Locations.Create(ctx, groupId, data)
return mappers.ToLocationOutErr(location, err)
func (svc *LocationService) Create(ctx context.Context, groupId uuid.UUID, data repo.LocationCreate) (repo.LocationOut, error) {
return svc.repos.Locations.Create(ctx, groupId, data)
}
func (svc *LocationService) Delete(ctx context.Context, groupId uuid.UUID, id uuid.UUID) error {
location, err := svc.repos.Locations.Get(ctx, id)
_, err := svc.repos.Locations.GetOneByGroup(ctx, groupId, id)
if err != nil {
return err
}
if location.Edges.Group.ID != groupId {
return ErrNotOwner
}
return svc.repos.Locations.Delete(ctx, id)
}
func (svc *LocationService) Update(ctx context.Context, groupId uuid.UUID, data types.LocationUpdate) (*types.LocationOut, error) {
location, err := svc.repos.Locations.Get(ctx, data.ID)
func (svc *LocationService) Update(ctx context.Context, groupId uuid.UUID, data repo.LocationUpdate) (repo.LocationOut, error) {
location, err := svc.repos.Locations.GetOneByGroup(ctx, groupId, data.ID)
if err != nil {
return nil, err
}
if location.Edges.Group.ID != groupId {
return nil, ErrNotOwner
return repo.LocationOut{}, err
}
return mappers.ToLocationOutErr(svc.repos.Locations.Update(ctx, data))
data.ID = location.ID
return svc.repos.Locations.Update(ctx, data)
}

View file

@ -7,9 +7,8 @@ import (
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/repo"
"github.com/hay-kot/homebox/backend/internal/services/mappers"
"github.com/hay-kot/homebox/backend/internal/types"
"github.com/hay-kot/homebox/backend/pkgs/hasher"
"github.com/rs/zerolog/log"
)
var (
@ -23,18 +22,46 @@ type UserService struct {
repos *repo.AllRepos
}
type (
UserRegistration struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
GroupName string `json:"groupName"`
}
UserAuthTokenDetail struct {
Raw string `json:"raw"`
ExpiresAt time.Time `json:"expiresAt"`
}
UserAuthTokenCreate struct {
TokenHash []byte `json:"token"`
UserID uuid.UUID `json:"userId"`
ExpiresAt time.Time `json:"expiresAt"`
}
LoginForm struct {
Username string `json:"username"`
Password string `json:"password"`
}
)
// RegisterUser creates a new user and group in the data with the provided data. It also bootstraps the user's group
// with default Labels and Locations.
func (svc *UserService) RegisterUser(ctx context.Context, data types.UserRegistration) (*types.UserOut, error) {
func (svc *UserService) RegisterUser(ctx context.Context, data UserRegistration) (repo.UserOut, error) {
log.Debug().
Str("name", data.Name).
Str("email", data.Email).
Str("groupName", data.GroupName).
Msg("Registering new user")
group, err := svc.repos.Groups.Create(ctx, data.GroupName)
if err != nil {
return &types.UserOut{}, err
return repo.UserOut{}, err
}
hashed, _ := hasher.HashPassword(data.User.Password)
usrCreate := types.UserCreate{
Name: data.User.Name,
Email: data.User.Email,
hashed, _ := hasher.HashPassword(data.Password)
usrCreate := repo.UserCreate{
Name: data.Name,
Email: data.Email,
Password: hashed,
IsSuperuser: false,
GroupID: group.ID,
@ -42,61 +69,61 @@ func (svc *UserService) RegisterUser(ctx context.Context, data types.UserRegistr
usr, err := svc.repos.Users.Create(ctx, usrCreate)
if err != nil {
return &types.UserOut{}, err
return repo.UserOut{}, err
}
for _, label := range defaultLabels() {
_, err := svc.repos.Labels.Create(ctx, group.ID, label)
if err != nil {
return &types.UserOut{}, err
return repo.UserOut{}, err
}
}
for _, location := range defaultLocations() {
_, err := svc.repos.Locations.Create(ctx, group.ID, location)
if err != nil {
return &types.UserOut{}, err
return repo.UserOut{}, err
}
}
return mappers.ToOutUser(usr, nil)
return usr, nil
}
// GetSelf returns the user that is currently logged in based of the token provided within
func (svc *UserService) GetSelf(ctx context.Context, requestToken string) (*types.UserOut, error) {
func (svc *UserService) GetSelf(ctx context.Context, requestToken string) (repo.UserOut, error) {
hash := hasher.HashToken(requestToken)
return mappers.ToOutUser(svc.repos.AuthTokens.GetUserFromToken(ctx, hash))
return svc.repos.AuthTokens.GetUserFromToken(ctx, hash)
}
func (svc *UserService) UpdateSelf(ctx context.Context, ID uuid.UUID, data types.UserUpdate) (*types.UserOut, error) {
func (svc *UserService) UpdateSelf(ctx context.Context, ID uuid.UUID, data repo.UserUpdate) (repo.UserOut, error) {
err := svc.repos.Users.Update(ctx, ID, data)
if err != nil {
return &types.UserOut{}, err
return repo.UserOut{}, err
}
return mappers.ToOutUser(svc.repos.Users.GetOneId(ctx, ID))
return svc.repos.Users.GetOneId(ctx, ID)
}
// ============================================================================
// User Authentication
func (svc *UserService) createToken(ctx context.Context, userId uuid.UUID) (types.UserAuthTokenDetail, error) {
func (svc *UserService) createToken(ctx context.Context, userId uuid.UUID) (UserAuthTokenDetail, error) {
newToken := hasher.GenerateToken()
created, err := svc.repos.AuthTokens.CreateToken(ctx, types.UserAuthTokenCreate{
created, err := svc.repos.AuthTokens.CreateToken(ctx, repo.UserAuthTokenCreate{
UserID: userId,
TokenHash: newToken.Hash,
ExpiresAt: time.Now().Add(oneWeek),
})
return types.UserAuthTokenDetail{Raw: newToken.Raw, ExpiresAt: created.ExpiresAt}, err
return UserAuthTokenDetail{Raw: newToken.Raw, ExpiresAt: created.ExpiresAt}, err
}
func (svc *UserService) Login(ctx context.Context, username, password string) (types.UserAuthTokenDetail, error) {
func (svc *UserService) Login(ctx context.Context, username, password string) (UserAuthTokenDetail, error) {
usr, err := svc.repos.Users.GetOneEmail(ctx, username)
if err != nil || !hasher.CheckPasswordHash(password, usr.Password) {
return types.UserAuthTokenDetail{}, ErrorInvalidLogin
if err != nil || !hasher.CheckPasswordHash(password, usr.PasswordHash) {
return UserAuthTokenDetail{}, ErrorInvalidLogin
}
return svc.createToken(ctx, usr.ID)
@ -108,13 +135,13 @@ func (svc *UserService) Logout(ctx context.Context, token string) error {
return err
}
func (svc *UserService) RenewToken(ctx context.Context, token string) (types.UserAuthTokenDetail, error) {
func (svc *UserService) RenewToken(ctx context.Context, token string) (UserAuthTokenDetail, error) {
hash := hasher.HashToken(token)
dbToken, err := svc.repos.AuthTokens.GetUserFromToken(ctx, hash)
if err != nil {
return types.UserAuthTokenDetail{}, ErrorInvalidToken
return UserAuthTokenDetail{}, ErrorInvalidToken
}
newToken, _ := svc.createToken(ctx, dbToken.ID)

View file

@ -1,9 +1,11 @@
package services
import "github.com/hay-kot/homebox/backend/internal/types"
import (
"github.com/hay-kot/homebox/backend/internal/repo"
)
func defaultLocations() []types.LocationCreate {
return []types.LocationCreate{
func defaultLocations() []repo.LocationCreate {
return []repo.LocationCreate{
{
Name: "Living Room",
},
@ -31,8 +33,8 @@ func defaultLocations() []types.LocationCreate {
}
}
func defaultLabels() []types.LabelCreate {
return []types.LabelCreate{
func defaultLabels() []repo.LabelCreate {
return []repo.LabelCreate{
{
Name: "Appliances",
},

View file

@ -1,18 +0,0 @@
package types
// ApiSummary
//
// @public
type ApiSummary struct {
Healthy bool `json:"health"`
Versions []string `json:"versions"`
Title string `json:"title"`
Message string `json:"message"`
Build Build
}
type Build struct {
Version string `json:"version"`
Commit string `json:"commit"`
BuildTime string `json:"buildTime"`
}

View file

@ -1,31 +0,0 @@
package types
import (
"time"
"github.com/google/uuid"
)
type DocumentOut struct {
ID uuid.UUID `json:"id"`
Title string `json:"title"`
Path string
}
type DocumentCreate struct {
Title string `json:"name"`
Path string `json:"path"`
}
type DocumentUpdate = DocumentCreate
type DocumentToken struct {
Raw string `json:"raw"`
ExpiresAt time.Time `json:"expiresAt"`
}
type DocumentTokenCreate struct {
TokenHash []byte `json:"tokenHash"`
DocumentID uuid.UUID `json:"documentId"`
ExpiresAt time.Time `json:"expiresAt"`
}

View file

@ -1,118 +0,0 @@
package types
import (
"time"
"github.com/google/uuid"
)
type ItemCreate struct {
ImportRef string `json:"-"`
Name string `json:"name"`
Description string `json:"description"`
// Edges
LocationID uuid.UUID `json:"locationId"`
LabelIDs []uuid.UUID `json:"labelIds"`
}
type ItemUpdate struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Quantity int `json:"quantity"`
Insured bool `json:"insured"`
// Edges
LocationID uuid.UUID `json:"locationId"`
LabelIDs []uuid.UUID `json:"labelIds"`
// Identifications
SerialNumber string `json:"serialNumber"`
ModelNumber string `json:"modelNumber"`
Manufacturer string `json:"manufacturer"`
// Warranty
LifetimeWarranty bool `json:"lifetimeWarranty"`
WarrantyExpires time.Time `json:"warrantyExpires"`
WarrantyDetails string `json:"warrantyDetails"`
// Purchase
PurchaseTime time.Time `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"`
PurchasePrice float64 `json:"purchasePrice,string"`
// Sold
SoldTime time.Time `json:"soldTime"`
SoldTo string `json:"soldTo"`
SoldPrice float64 `json:"soldPrice,string"`
SoldNotes string `json:"soldNotes"`
// Extras
Notes string `json:"notes"`
// Fields []*FieldSummary `json:"fields"`
}
type ItemSummary struct {
ImportRef string `json:"-"`
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Quantity int `json:"quantity"`
Insured bool `json:"insured"`
// Edges
Location *LocationSummary `json:"location"`
Labels []*LabelSummary `json:"labels"`
// Identifications
SerialNumber string `json:"serialNumber"`
ModelNumber string `json:"modelNumber"`
Manufacturer string `json:"manufacturer"`
// Warranty
LifetimeWarranty bool `json:"lifetimeWarranty"`
WarrantyExpires time.Time `json:"warrantyExpires"`
WarrantyDetails string `json:"warrantyDetails"`
// Purchase
PurchaseTime time.Time `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"`
PurchasePrice float64 `json:"purchasePrice,string"`
// Sold
SoldTime time.Time `json:"soldTime"`
SoldTo string `json:"soldTo"`
SoldPrice float64 `json:"soldPrice,string"`
SoldNotes string `json:"soldNotes"`
// Extras
Notes string `json:"notes"`
}
type ItemOut struct {
ItemSummary
Attachments []*ItemAttachment `json:"attachments"`
// Future
// Fields []*FieldSummary `json:"fields"`
}
type ItemAttachment struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Type string `json:"type"`
Document DocumentOut `json:"document"`
}
type ItemAttachmentToken struct {
Token string `json:"token"`
}
type ItemAttachmentUpdate struct {
ID uuid.UUID `json:"-"`
Type string `json:"type"`
Title string `json:"title"`
}

View file

@ -1,33 +0,0 @@
package types
import (
"time"
"github.com/google/uuid"
)
type LabelCreate struct {
Name string `json:"name"`
Description string `json:"description"`
Color string `json:"color"`
}
type LabelUpdate struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Color string `json:"color"`
}
type LabelSummary struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type LabelOut struct {
LabelSummary
Items []*ItemSummary `json:"items"`
}

View file

@ -1,36 +0,0 @@
package types
import (
"time"
"github.com/google/uuid"
)
type LocationCreate struct {
Name string `json:"name"`
Description string `json:"description"`
}
type LocationUpdate struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
type LocationSummary struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type LocationCount struct {
LocationSummary
ItemCount int `json:"itemCount"`
}
type LocationOut struct {
LocationSummary
Items []*ItemSummary `json:"items"`
}

View file

@ -1,39 +0,0 @@
package types
import (
"time"
"github.com/google/uuid"
)
type LoginForm struct {
Username string `json:"username"`
Password string `json:"password"`
}
type TokenResponse struct {
BearerToken string `json:"token"`
ExpiresAt time.Time `json:"expiresAt"`
}
type UserAuthTokenDetail struct {
Raw string `json:"raw"`
ExpiresAt time.Time `json:"expiresAt"`
}
type UserAuthToken struct {
TokenHash []byte `json:"token"`
UserID uuid.UUID `json:"userId"`
ExpiresAt time.Time `json:"expiresAt"`
CreatedAt time.Time `json:"createdAt"`
}
func (u UserAuthToken) IsExpired() bool {
return u.ExpiresAt.Before(time.Now())
}
type UserAuthTokenCreate struct {
TokenHash []byte `json:"token"`
UserID uuid.UUID `json:"userId"`
ExpiresAt time.Time `json:"expiresAt"`
}

View file

@ -1,60 +0,0 @@
package types
import (
"errors"
"github.com/google/uuid"
)
var (
ErrNameEmpty = errors.New("name is empty")
ErrEmailEmpty = errors.New("email is empty")
)
// UserIn is a basic user input struct containing only the fields that are
// required for user creation.
type UserIn struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
}
// UserCreate is the Data object contain the requirements of creating a user
// in the database. It should to create users from an API unless the user has
// rights to create SuperUsers. For regular user in data use the UserIn struct.
type UserCreate struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
IsSuperuser bool `json:"isSuperuser"`
GroupID uuid.UUID `json:"groupID"`
}
func (u UserCreate) Validate() error {
if u.Name == "" {
return ErrNameEmpty
}
if u.Email == "" {
return ErrEmailEmpty
}
return nil
}
type UserUpdate struct {
Name string `json:"name"`
Email string `json:"email"`
}
type UserRegistration struct {
User UserIn `json:"user"`
GroupName string `json:"groupName"`
}
type UserOut struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
IsSuperuser bool `json:"isSuperuser"`
GroupID uuid.UUID `json:"groupId"`
GroupName string `json:"groupName"`
}

View file

@ -1,63 +0,0 @@
package types
import (
"testing"
)
func TestUserCreate_Validate(t *testing.T) {
type fields struct {
Name string
Email string
Password string
IsSuperuser bool
}
tests := []struct {
name string
fields fields
wantErr bool
}{
{
name: "no_name",
fields: fields{
Name: "",
Email: "",
Password: "",
IsSuperuser: false,
},
wantErr: true,
},
{
name: "no_email",
fields: fields{
Name: "test",
Email: "",
Password: "",
IsSuperuser: false,
},
wantErr: true,
},
{
name: "valid",
fields: fields{
Name: "test",
Email: "test@email.com",
Password: "mypassword",
IsSuperuser: false,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u := &UserCreate{
Name: tt.fields.Name,
Email: tt.fields.Email,
Password: tt.fields.Password,
IsSuperuser: tt.fields.IsSuperuser,
}
if err := u.Validate(); (err != nil) != tt.wantErr {
t.Errorf("UserCreate.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

101
backend/pkgs/set/funcs.go Normal file
View file

@ -0,0 +1,101 @@
package set
// Diff returns the difference between two sets
func Diff[T key](a, b Set[T]) Set[T] {
s := New[T]()
for k := range a.mp {
if !b.Contains(k) {
s.Insert(k)
}
}
return s
}
// Intersect returns the intersection between two sets
func Intersect[T key](a, b Set[T]) Set[T] {
s := New[T]()
for k := range a.mp {
if b.Contains(k) {
s.Insert(k)
}
}
return s
}
// Union returns the union between two sets
func Union[T key](a, b Set[T]) Set[T] {
s := New[T]()
for k := range a.mp {
s.Insert(k)
}
for k := range b.mp {
s.Insert(k)
}
return s
}
// Xor returns the symmetric difference between two sets
func Xor[T key](a, b Set[T]) Set[T] {
s := New[T]()
for k := range a.mp {
if !b.Contains(k) {
s.Insert(k)
}
}
for k := range b.mp {
if !a.Contains(k) {
s.Insert(k)
}
}
return s
}
// Equal returns true if two sets are equal
func Equal[T key](a, b Set[T]) bool {
if a.Len() != b.Len() {
return false
}
for k := range a.mp {
if !b.Contains(k) {
return false
}
}
return true
}
// Subset returns true if a is a subset of b
func Subset[T key](a, b Set[T]) bool {
if a.Len() > b.Len() {
return false
}
for k := range a.mp {
if !b.Contains(k) {
return false
}
}
return true
}
// Superset returns true if a is a superset of b
func Superset[T key](a, b Set[T]) bool {
if a.Len() < b.Len() {
return false
}
for k := range b.mp {
if !a.Contains(k) {
return false
}
}
return true
}
// Disjoint returns true if two sets are disjoint
func Disjoint[T key](a, b Set[T]) bool {
for k := range a.mp {
if b.Contains(k) {
return false
}
}
return true
}

View file

@ -0,0 +1,287 @@
package set
import (
"reflect"
"testing"
)
type args struct {
a Set[string]
b Set[string]
}
var (
argsBasic = args{
a: New("a", "b", "c"),
b: New("b", "c", "d"),
}
argsNoOverlap = args{
a: New("a", "b", "c"),
b: New("d", "e", "f"),
}
argsIdentical = args{
a: New("a", "b", "c"),
b: New("a", "b", "c"),
}
)
func TestDiff(t *testing.T) {
tests := []struct {
name string
args args
want Set[string]
}{
{
name: "diff basic",
args: argsBasic,
want: New("a"),
},
{
name: "diff empty",
args: argsIdentical,
want: New[string](),
},
{
name: "diff no overlap",
args: argsNoOverlap,
want: New("a", "b", "c"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Diff(tt.args.a, tt.args.b); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Diff() = %v, want %v", got, tt.want)
}
})
}
}
func TestIntersect(t *testing.T) {
tests := []struct {
name string
args args
want Set[string]
}{
{
name: "intersect basic",
args: argsBasic,
want: New("b", "c"),
},
{
name: "identical sets",
args: argsIdentical,
want: New("a", "b", "c"),
},
{
name: "no overlap",
args: argsNoOverlap,
want: New[string](),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Intersect(tt.args.a, tt.args.b); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Intersect() = %v, want %v", got, tt.want)
}
})
}
}
func TestUnion(t *testing.T) {
tests := []struct {
name string
args args
want Set[string]
}{
{
name: "intersect basic",
args: argsBasic,
want: New("a", "b", "c", "d"),
},
{
name: "identical sets",
args: argsIdentical,
want: New("a", "b", "c"),
},
{
name: "no overlap",
args: argsNoOverlap,
want: New("a", "b", "c", "d", "e", "f"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Union(tt.args.a, tt.args.b); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Union() = %v, want %v", got, tt.want)
}
})
}
}
func TestXor(t *testing.T) {
tests := []struct {
name string
args args
want Set[string]
}{
{
name: "xor basic",
args: argsBasic,
want: New("a", "d"),
},
{
name: "identical sets",
args: argsIdentical,
want: New[string](),
},
{
name: "no overlap",
args: argsNoOverlap,
want: New("a", "b", "c", "d", "e", "f"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Xor(tt.args.a, tt.args.b); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Xor() = %v, want %v", got, tt.want)
}
})
}
}
func TestEqual(t *testing.T) {
tests := []struct {
name string
args args
want bool
}{
{
name: "equal basic",
args: argsBasic,
want: false,
},
{
name: "identical sets",
args: argsIdentical,
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Equal(tt.args.a, tt.args.b); got != tt.want {
t.Errorf("Equal() = %v, want %v", got, tt.want)
}
})
}
}
func TestSubset(t *testing.T) {
type args struct {
a Set[string]
b Set[string]
}
tests := []struct {
name string
args args
want bool
}{
{
name: "subset basic",
args: args{
a: New("a", "b"),
b: New("a", "b", "c"),
},
want: true,
},
{
name: "subset basic false",
args: args{
a: New("a", "b", "d"),
b: New("a", "b", "c"),
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Subset(tt.args.a, tt.args.b); got != tt.want {
t.Errorf("Subset() = %v, want %v", got, tt.want)
}
})
}
}
func TestSuperset(t *testing.T) {
type args struct {
a Set[string]
b Set[string]
}
tests := []struct {
name string
args args
want bool
}{
{
name: "superset basic",
args: args{
a: New("a", "b", "c"),
b: New("a", "b"),
},
want: true,
},
{
name: "superset basic false",
args: args{
a: New("a", "b", "c"),
b: New("a", "b", "d"),
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Superset(tt.args.a, tt.args.b); got != tt.want {
t.Errorf("Superset() = %v, want %v", got, tt.want)
}
})
}
}
func TestDisjoint(t *testing.T) {
type args struct {
a Set[string]
b Set[string]
}
tests := []struct {
name string
args args
want bool
}{
{
name: "disjoint basic",
args: args{
a: New("a", "b"),
b: New("c", "d"),
},
want: true,
},
{
name: "disjoint basic false",
args: args{
a: New("a", "b"),
b: New("b", "c"),
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Disjoint(tt.args.a, tt.args.b); got != tt.want {
t.Errorf("Disjoint() = %v, want %v", got, tt.want)
}
})
}
}

56
backend/pkgs/set/set.go Normal file
View file

@ -0,0 +1,56 @@
package set
type key interface {
comparable
}
type Set[T key] struct {
mp map[T]struct{}
}
func New[T key](v ...T) Set[T] {
mp := make(map[T]struct{}, len(v))
s := Set[T]{mp}
s.Insert(v...)
return s
}
func (s Set[T]) Insert(v ...T) {
for _, e := range v {
s.mp[e] = struct{}{}
}
}
func (s Set[T]) Remove(v ...T) {
for _, e := range v {
delete(s.mp, e)
}
}
func (s Set[T]) Contains(v T) bool {
_, ok := s.mp[v]
return ok
}
func (s Set[T]) ContainsAll(v ...T) bool {
for _, e := range v {
if !s.Contains(e) {
return false
}
}
return true
}
func (s Set[T]) Slice() []T {
slice := make([]T, 0, len(s.mp))
for k := range s.mp {
slice = append(slice, k)
}
return slice
}
func (s Set[T]) Len() int {
return len(s.mp)
}

View file

@ -0,0 +1,255 @@
package set
import (
"reflect"
"testing"
)
func TestNew(t *testing.T) {
type args struct {
v []string
}
tests := []struct {
name string
args args
want Set[string]
}{
{
name: "new",
args: args{
v: []string{"a", "b", "c"},
},
want: Set[string]{
mp: map[string]struct{}{
"a": {},
"b": {},
"c": {},
},
},
},
{
name: "new empty",
args: args{
v: []string{},
},
want: Set[string]{
mp: map[string]struct{}{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := New(tt.args.v...); !reflect.DeepEqual(got, tt.want) {
t.Errorf("New() = %v, want %v", got, tt.want)
}
})
}
}
func TestSet_Insert(t *testing.T) {
type args struct {
v []string
}
tests := []struct {
name string
s Set[string]
args args
want Set[string]
}{
{
name: "insert",
s: Set[string]{
mp: map[string]struct{}{
"a": {},
"b": {},
"c": {},
},
},
args: args{
v: []string{"d", "e", "f"},
},
want: Set[string]{
mp: map[string]struct{}{
"a": {},
"b": {},
"c": {},
"d": {},
"e": {},
"f": {},
},
},
},
{
name: "insert empty",
s: Set[string]{
mp: map[string]struct{}{},
},
args: args{
v: []string{"a", "b", "c"},
},
want: Set[string]{
mp: map[string]struct{}{
"a": {},
"b": {},
"c": {},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.s.Insert(tt.args.v...)
if !reflect.DeepEqual(tt.s, tt.want) {
t.Errorf("Set.Insert() = %v, want %v", tt.s, tt.want)
}
})
}
}
func TestSet_Delete(t *testing.T) {
type args struct {
v []string
}
tests := []struct {
name string
s Set[string]
args args
want Set[string]
}{
{
name: "insert",
s: Set[string]{
mp: map[string]struct{}{
"a": {},
"b": {},
"c": {},
"d": {},
"e": {},
"f": {},
},
},
args: args{
v: []string{"d", "e", "f"},
},
want: Set[string]{
mp: map[string]struct{}{
"a": {},
"b": {},
"c": {},
},
},
},
{
name: "delete empty",
s: Set[string]{
mp: map[string]struct{}{},
},
args: args{
v: []string{},
},
want: Set[string]{
mp: map[string]struct{}{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.s.Remove(tt.args.v...)
if !reflect.DeepEqual(tt.s, tt.want) {
t.Errorf("Set.Delete() = %v, want %v", tt.s, tt.want)
}
})
}
}
func TestSet_ContainsAll(t *testing.T) {
type args struct {
v []string
}
tests := []struct {
name string
s Set[string]
args args
want bool
}{
{
name: "contains",
s: Set[string]{
mp: map[string]struct{}{
"a": {},
"b": {},
"c": {},
},
},
args: args{
v: []string{"a", "b", "c"},
},
want: true,
},
{
name: "contains empty",
s: Set[string]{
mp: map[string]struct{}{},
},
args: args{
v: []string{},
},
want: true,
},
{
name: "not contains",
s: Set[string]{
mp: map[string]struct{}{
"a": {},
"b": {},
"c": {},
},
},
args: args{
v: []string{"d", "e", "f"},
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.s.ContainsAll(tt.args.v...); got != tt.want {
t.Errorf("Set.ContainsAll() = %v, want %v", got, tt.want)
}
})
}
}
func TestSet_Slice(t *testing.T) {
tests := []struct {
name string
s Set[string]
want []string
}{
{
name: "slice",
s: Set[string]{
mp: map[string]struct{}{
"a": {},
"b": {},
"c": {},
},
},
want: []string{"a", "b", "c"},
},
{
name: "slice empty",
s: Set[string]{
mp: map[string]struct{}{},
},
want: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.s.Slice(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Set.Slice() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -26,11 +26,11 @@
</template>
<script lang="ts" setup>
import { LocationCount } from "~~/lib/api/types/data-contracts";
import { LocationOutCount } from "~~/lib/api/types/data-contracts";
defineProps({
location: {
type: Object as () => LocationCount,
type: Object as () => LocationOutCount,
required: true,
},
dense: {

View file

@ -5,7 +5,7 @@ import { Ref } from "vue";
type Store = UseConfirmDialogReturn<any, boolean, boolean> & {
text: Ref<string>;
setup: boolean;
open: (text: string) => Promise<UseConfirmDialogRevealResult<boolean, boolean>>;
open: (text: string) => Promise<UseConfirmDialogRevealResult<boolean, boolean>>;
};
const store: Partial<Store> = {
@ -31,7 +31,7 @@ export function useConfirm(): Store {
store.cancel = cancel;
}
async function openDialog(msg: string): Promise<UseConfirmDialogRevealResult<boolean, boolean>> {
async function openDialog(msg: string): Promise<UseConfirmDialogRevealResult<boolean, boolean>> {
store.text.value = msg;
return await store.reveal();
}

View file

@ -14,11 +14,9 @@ describe("first time user workflow (register, login)", () => {
const api = client();
const userData = {
groupName: "test-group",
user: {
email: "test-user@email.com",
name: "test-user",
password: "test-password",
},
email: "test-user@email.com",
name: "test-user",
password: "test-password",
};
test("user should be able to register", async () => {
@ -27,7 +25,7 @@ describe("first time user workflow (register, login)", () => {
});
test("user should be able to login", async () => {
const { response, data } = await api.login(userData.user.email, userData.user.password);
const { response, data } = await api.login(userData.email, userData.password);
expect(response.status).toBe(200);
expect(data.token).toBeTruthy();

View file

@ -31,15 +31,13 @@ export async function sharedUserClient(): Promise<UserApi> {
}
const testUser = {
groupName: "test-group",
user: {
email: "__test__@__test__.com",
name: "__test__",
password: "__test__",
},
email: "__test__@__test__.com",
name: "__test__",
password: "__test__",
};
const api = client();
const { response: tryLoginResp, data } = await api.login(testUser.user.email, testUser.user.password);
const { response: tryLoginResp, data } = await api.login(testUser.email, testUser.password);
if (tryLoginResp.status === 200) {
cache.token = data.token;
@ -49,7 +47,7 @@ export async function sharedUserClient(): Promise<UserApi> {
const { response: registerResp } = await api.register(testUser);
expect(registerResp.status).toBe(204);
const { response: loginResp, data: loginData } = await api.login(testUser.user.email, testUser.user.password);
const { response: loginResp, data: loginData } = await api.login(testUser.email, testUser.password);
expect(loginResp.status).toBe(200);
cache.token = loginData.token;

View file

@ -1,12 +1,12 @@
import { BaseAPI, route } from "../base";
import { LocationCount, LocationCreate, LocationOut } from "../types/data-contracts";
import { LocationOutCount, LocationCreate, LocationOut } from "../types/data-contracts";
import { Results } from "./types";
export type LocationUpdate = LocationCreate;
export class LocationsApi extends BaseAPI {
getAll() {
return this.http.get<Results<LocationCount>>({ url: route("/locations") });
return this.http.get<Results<LocationOutCount>>({ url: route("/locations") });
}
create(body: LocationCreate) {

View file

@ -1,24 +1,11 @@
import { BaseAPI, route } from "./base";
export type LoginResult = {
token: string;
expiresAt: string;
};
import { ApiSummary, TokenResponse, UserRegistration } from "./types/data-contracts";
export type LoginPayload = {
username: string;
password: string;
};
export type RegisterPayload = {
user: {
email: string;
password: string;
name: string;
};
groupName: string;
};
export type StatusResult = {
health: boolean;
versions: string[];
@ -28,11 +15,11 @@ export type StatusResult = {
export class PublicApi extends BaseAPI {
public status() {
return this.http.get<StatusResult>({ url: route("/status") });
return this.http.get<ApiSummary>({ url: route("/status") });
}
public login(username: string, password: string) {
return this.http.post<LoginPayload, LoginResult>({
return this.http.post<LoginPayload, TokenResponse>({
url: route("/users/login"),
body: {
username,
@ -41,7 +28,7 @@ export class PublicApi extends BaseAPI {
});
}
public register(body: RegisterPayload) {
return this.http.post<RegisterPayload, LoginResult>({ url: route("/users/register"), body });
public register(body: UserRegistration) {
return this.http.post<UserRegistration, TokenResponse>({ url: route("/users/register"), body });
}
}

View file

@ -10,36 +10,6 @@
* ---------------------------------------------------------------
*/
export interface ServerResult {
details: any;
error: boolean;
item: any;
message: string;
}
export interface ServerResults {
items: any;
}
export interface ServerValidationError {
field: string;
reason: string;
}
export interface ApiSummary {
build: Build;
health: boolean;
message: string;
title: string;
versions: string[];
}
export interface Build {
buildTime: string;
commit: string;
version: string;
}
export interface DocumentOut {
id: string;
path: string;
@ -54,10 +24,6 @@ export interface ItemAttachment {
updatedAt: Date;
}
export interface ItemAttachmentToken {
token: string;
}
export interface ItemAttachmentUpdate {
title: string;
type: string;
@ -99,8 +65,6 @@ export interface ItemOut {
/** Purchase */
purchaseTime: Date;
quantity: number;
/** Identifications */
serialNumber: string;
soldNotes: string;
@ -122,39 +86,11 @@ export interface ItemSummary {
insured: boolean;
labels: LabelSummary[];
/** Warranty */
lifetimeWarranty: boolean;
/** Edges */
location: LocationSummary;
manufacturer: string;
modelNumber: string;
name: string;
/** Extras */
notes: string;
purchaseFrom: string;
/** @example 0 */
purchasePrice: string;
/** Purchase */
purchaseTime: Date;
quantity: number;
/** Identifications */
serialNumber: string;
soldNotes: string;
/** @example 0 */
soldPrice: string;
/** Sold */
soldTime: Date;
soldTo: string;
updatedAt: Date;
warrantyDetails: string;
warrantyExpires: Date;
}
export interface ItemUpdate {
@ -220,15 +156,6 @@ export interface LabelSummary {
updatedAt: Date;
}
export interface LocationCount {
createdAt: Date;
description: string;
id: string;
itemCount: number;
name: string;
updatedAt: Date;
}
export interface LocationCreate {
description: string;
name: string;
@ -243,6 +170,15 @@ export interface LocationOut {
updatedAt: Date;
}
export interface LocationOutCount {
createdAt: Date;
description: string;
id: string;
itemCount: number;
name: string;
updatedAt: Date;
}
export interface LocationSummary {
createdAt: Date;
description: string;
@ -251,17 +187,6 @@ export interface LocationSummary {
updatedAt: Date;
}
export interface TokenResponse {
expiresAt: string;
token: string;
}
export interface UserIn {
email: string;
name: string;
password: string;
}
export interface UserOut {
email: string;
groupId: string;
@ -271,12 +196,53 @@ export interface UserOut {
name: string;
}
export interface UserRegistration {
groupName: string;
user: UserIn;
}
export interface UserUpdate {
email: string;
name: string;
}
export interface ServerResult {
details: any;
error: boolean;
item: any;
message: string;
}
export interface ServerResults {
items: any;
}
export interface ServerValidationError {
field: string;
reason: string;
}
export interface UserRegistration {
email: string;
groupName: string;
name: string;
password: string;
}
export interface ApiSummary {
build: Build;
health: boolean;
message: string;
title: string;
versions: string[];
}
export interface Build {
buildTime: string;
commit: string;
version: string;
}
export interface ItemAttachmentToken {
token: string;
}
export interface TokenResponse {
expiresAt: string;
token: string;
}

View file

@ -25,11 +25,9 @@
async function registerUser() {
loading.value = true;
const { error } = await api.register({
user: {
name: username.value,
email: email.value,
password: password.value,
},
name: username.value,
email: email.value,
password: password.value,
groupName: groupName.value,
});

View file

@ -1,9 +1,9 @@
import { defineStore } from "pinia";
import { LocationCount } from "~~/lib/api/types/data-contracts";
import { LocationOutCount } from "~~/lib/api/types/data-contracts";
export const useLocationStore = defineStore("locations", {
state: () => ({
allLocations: null as LocationCount[] | null,
allLocations: null as LocationOutCount[] | null,
client: useUserApi(),
}),
getters: {
@ -12,7 +12,7 @@ export const useLocationStore = defineStore("locations", {
* synched with the server by intercepting the API calls and updating on the
* response
*/
locations(state): LocationCount[] {
locations(state): LocationOutCount[] {
if (state.allLocations === null) {
Promise.resolve(this.refresh());
}
@ -20,7 +20,7 @@ export const useLocationStore = defineStore("locations", {
},
},
actions: {
async refresh(): Promise<LocationCount[]> {
async refresh(): Promise<LocationOutCount[]> {
const result = await this.client.locations.getAll();
if (result.error) {
return result;

View file

@ -22,7 +22,9 @@ def date_types(*names: list[str]) -> dict[re.Pattern, str]:
regex_replace: dict[re.Pattern, str] = {
re.compile(r"Types"): "",
re.compile(r" Repo"): " ",
re.compile(r" Services"): " ",
re.compile(r" V1"): " ",
re.compile(r"\?:"): ":",
**date_types(
"createdAt",