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

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)
}
})
}
}