forked from mirrors/homebox
refactor: remove empty services (#116)
* remove empty services * remove old factory * remove old static files * cleanup more duplicate service code * file/folder reorg
This commit is contained in:
parent
6529549289
commit
cd82fe0d89
179 changed files with 514 additions and 582 deletions
21
backend/internal/data/repo/id_set.go
Normal file
21
backend/internal/data/repo/id_set.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package repo
|
||||
|
||||
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
|
||||
type HasID interface {
|
||||
GetID() uuid.UUID
|
||||
}
|
||||
|
||||
func newIDSet[T HasID](entities []T) set.Set[uuid.UUID] {
|
||||
uuids := make([]uuid.UUID, 0, len(entities))
|
||||
for _, e := range entities {
|
||||
uuids = append(uuids, e.GetID())
|
||||
}
|
||||
|
||||
return set.New(uuids...)
|
||||
}
|
62
backend/internal/data/repo/main_test.go
Normal file
62
backend/internal/data/repo/main_test.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/faker"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
var (
|
||||
fk = faker.NewFaker()
|
||||
|
||||
tClient *ent.Client
|
||||
tRepos *AllRepos
|
||||
tUser UserOut
|
||||
tGroup Group
|
||||
)
|
||||
|
||||
func bootstrap() {
|
||||
var (
|
||||
err error
|
||||
ctx = context.Background()
|
||||
)
|
||||
|
||||
tGroup, err = tRepos.Groups.GroupCreate(ctx, "test-group")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
tUser, err = tRepos.Users.Create(ctx, userFactory())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
rand.Seed(int64(time.Now().Unix()))
|
||||
|
||||
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
|
||||
if err != nil {
|
||||
log.Fatalf("failed opening connection to sqlite: %v", err)
|
||||
}
|
||||
|
||||
err = client.Schema.Create(context.Background())
|
||||
if err != nil {
|
||||
log.Fatalf("failed creating schema resources: %v", err)
|
||||
}
|
||||
|
||||
tClient = client
|
||||
tRepos = New(tClient, os.TempDir())
|
||||
defer client.Close()
|
||||
|
||||
bootstrap()
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
52
backend/internal/data/repo/map_helpers.go
Normal file
52
backend/internal/data/repo/map_helpers.go
Normal 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
|
||||
}
|
12
backend/internal/data/repo/pagination.go
Normal file
12
backend/internal/data/repo/pagination.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package repo
|
||||
|
||||
type PaginationResult[T any] struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Total int `json:"total"`
|
||||
Items []T `json:"items"`
|
||||
}
|
||||
|
||||
func calculateOffset(page, pageSize int) int {
|
||||
return (page - 1) * pageSize
|
||||
}
|
68
backend/internal/data/repo/repo_document_tokens.go
Normal file
68
backend/internal/data/repo/repo_document_tokens.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/documenttoken"
|
||||
)
|
||||
|
||||
// DocumentTokensRepository is a repository for Document entity
|
||||
type DocumentTokensRepository struct {
|
||||
db *ent.Client
|
||||
}
|
||||
|
||||
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).
|
||||
SetExpiresAt(data.ExpiresAt).
|
||||
Save(ctx)
|
||||
|
||||
if err != nil {
|
||||
return DocumentToken{}, err
|
||||
}
|
||||
|
||||
return mapDocumentTokenErr(r.db.DocumentToken.Query().
|
||||
Where(documenttoken.ID(result.ID)).
|
||||
WithDocument().
|
||||
Only(ctx))
|
||||
}
|
||||
|
||||
func (r *DocumentTokensRepository) PurgeExpiredTokens(ctx context.Context) (int, error) {
|
||||
return r.db.DocumentToken.Delete().Where(documenttoken.ExpiresAtLT(time.Now())).Exec(ctx)
|
||||
}
|
||||
|
||||
func (r *DocumentTokensRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return r.db.DocumentToken.DeleteOneID(id).Exec(ctx)
|
||||
}
|
150
backend/internal/data/repo/repo_document_tokens_test.go
Normal file
150
backend/internal/data/repo/repo_document_tokens_test.go
Normal file
|
@ -0,0 +1,150 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/documenttoken"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDocumentTokensRepository_Create(t *testing.T) {
|
||||
entities := useDocs(t, 1)
|
||||
doc := entities[0]
|
||||
expires := fk.Time()
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
data DocumentTokenCreate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *ent.DocumentToken
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "create document token",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: DocumentTokenCreate{
|
||||
DocumentID: doc.ID,
|
||||
TokenHash: []byte("token"),
|
||||
ExpiresAt: expires,
|
||||
},
|
||||
},
|
||||
want: &ent.DocumentToken{
|
||||
Edges: ent.DocumentTokenEdges{
|
||||
Document: &ent.Document{
|
||||
ID: doc.ID,
|
||||
},
|
||||
},
|
||||
Token: []byte("token"),
|
||||
ExpiresAt: expires,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "create document token with empty token",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: DocumentTokenCreate{
|
||||
DocumentID: doc.ID,
|
||||
TokenHash: []byte(""),
|
||||
ExpiresAt: expires,
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "create document token with empty document id",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: DocumentTokenCreate{
|
||||
DocumentID: uuid.Nil,
|
||||
TokenHash: []byte("token"),
|
||||
ExpiresAt: expires,
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
ids := make([]uuid.UUID, 0, len(tests))
|
||||
|
||||
t.Cleanup(func() {
|
||||
for _, id := range ids {
|
||||
_ = tRepos.DocTokens.Delete(context.Background(), id)
|
||||
}
|
||||
})
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
got, err := tRepos.DocTokens.Create(tt.args.ctx, tt.args.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("DocumentTokensRepository.Create() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if tt.wantErr {
|
||||
return
|
||||
}
|
||||
|
||||
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.DocumentID)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func useDocTokens(t *testing.T, num int) []DocumentToken {
|
||||
entity := useDocs(t, 1)[0]
|
||||
|
||||
results := make([]DocumentToken, 0, num)
|
||||
|
||||
ids := make([]uuid.UUID, 0, num)
|
||||
t.Cleanup(func() {
|
||||
for _, id := range ids {
|
||||
_ = tRepos.DocTokens.Delete(context.Background(), id)
|
||||
}
|
||||
})
|
||||
|
||||
for i := 0; i < num; i++ {
|
||||
e, err := tRepos.DocTokens.Create(context.Background(), DocumentTokenCreate{
|
||||
DocumentID: entity.ID,
|
||||
TokenHash: []byte(fk.Str(10)),
|
||||
ExpiresAt: fk.Time(),
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
results = append(results, e)
|
||||
ids = append(ids, e.ID)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func TestDocumentTokensRepository_PurgeExpiredTokens(t *testing.T) {
|
||||
entities := useDocTokens(t, 2)
|
||||
|
||||
// set expired token
|
||||
tRepos.DocTokens.db.DocumentToken.Update().
|
||||
Where(documenttoken.ID(entities[0].ID)).
|
||||
SetExpiresAt(time.Now().Add(-time.Hour)).
|
||||
ExecX(context.Background())
|
||||
|
||||
count, err := tRepos.DocTokens.PurgeExpiredTokens(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, count)
|
||||
|
||||
all, err := tRepos.DocTokens.db.DocumentToken.Query().All(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, all, 1)
|
||||
assert.Equal(t, entities[1].ID, all[0].ID)
|
||||
}
|
118
backend/internal/data/repo/repo_documents.go
Normal file
118
backend/internal/data/repo/repo_documents.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/document"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/group"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/pathlib"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidDocExtension = errors.New("invalid document extension")
|
||||
)
|
||||
|
||||
type DocumentRepository struct {
|
||||
db *ent.Client
|
||||
dir string
|
||||
}
|
||||
|
||||
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(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)
|
||||
}
|
111
backend/internal/data/repo/repo_documents_test.go
Normal file
111
backend/internal/data/repo/repo_documents_test.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func useDocs(t *testing.T, num int) []DocumentOut {
|
||||
t.Helper()
|
||||
|
||||
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, DocumentCreate{
|
||||
Title: fk.Str(10) + ".md",
|
||||
Content: bytes.NewReader([]byte(fk.Str(10))),
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, doc)
|
||||
results = append(results, doc)
|
||||
ids = append(ids, doc.ID)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
for _, id := range ids {
|
||||
err := tRepos.Docs.Delete(context.Background(), id)
|
||||
|
||||
if err != nil {
|
||||
assert.True(t, ent.IsNotFound(err))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func TestDocumentRepository_CreateUpdateDelete(t *testing.T) {
|
||||
temp := t.TempDir()
|
||||
r := DocumentRepository{
|
||||
db: tClient,
|
||||
dir: temp,
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
ensureRead := func() {
|
||||
// Read Document
|
||||
bts, err := os.ReadFile(got.Path)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.content, string(bts))
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
131
backend/internal/data/repo/repo_group.go
Normal file
131
backend/internal/data/repo/repo_group.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/group"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/groupinvitationtoken"
|
||||
)
|
||||
|
||||
type GroupRepository struct {
|
||||
db *ent.Client
|
||||
}
|
||||
|
||||
type (
|
||||
Group struct {
|
||||
ID uuid.UUID `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
UpdatedAt time.Time `json:"updatedAt,omitempty"`
|
||||
Currency string `json:"currency,omitempty"`
|
||||
}
|
||||
|
||||
GroupUpdate struct {
|
||||
Name string `json:"name"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
GroupInvitationCreate struct {
|
||||
Token []byte `json:"-"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
Uses int `json:"uses"`
|
||||
}
|
||||
|
||||
GroupInvitation struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
Uses int `json:"uses"`
|
||||
Group Group `json:"group"`
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
mapToGroupErr = mapTErrFunc(mapToGroup)
|
||||
)
|
||||
|
||||
func mapToGroup(g *ent.Group) Group {
|
||||
return Group{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
CreatedAt: g.CreatedAt,
|
||||
UpdatedAt: g.UpdatedAt,
|
||||
Currency: strings.ToUpper(g.Currency.String()),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
mapToGroupInvitationErr = mapTErrFunc(mapToGroupInvitation)
|
||||
)
|
||||
|
||||
func mapToGroupInvitation(g *ent.GroupInvitationToken) GroupInvitation {
|
||||
return GroupInvitation{
|
||||
ID: g.ID,
|
||||
ExpiresAt: g.ExpiresAt,
|
||||
Uses: g.Uses,
|
||||
Group: mapToGroup(g.Edges.Group),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *GroupRepository) GroupCreate(ctx context.Context, name string) (Group, error) {
|
||||
return mapToGroupErr(r.db.Group.Create().
|
||||
SetName(name).
|
||||
Save(ctx))
|
||||
}
|
||||
|
||||
func (r *GroupRepository) GroupUpdate(ctx context.Context, ID uuid.UUID, data GroupUpdate) (Group, error) {
|
||||
currency := group.Currency(strings.ToLower(data.Currency))
|
||||
|
||||
entity, err := r.db.Group.UpdateOneID(ID).
|
||||
SetName(data.Name).
|
||||
SetCurrency(currency).
|
||||
Save(ctx)
|
||||
|
||||
return mapToGroupErr(entity, err)
|
||||
}
|
||||
|
||||
func (r *GroupRepository) GroupByID(ctx context.Context, id uuid.UUID) (Group, error) {
|
||||
return mapToGroupErr(r.db.Group.Get(ctx, id))
|
||||
}
|
||||
|
||||
func (r *GroupRepository) InvitationGet(ctx context.Context, token []byte) (GroupInvitation, error) {
|
||||
return mapToGroupInvitationErr(r.db.GroupInvitationToken.Query().
|
||||
Where(groupinvitationtoken.Token(token)).
|
||||
WithGroup().
|
||||
Only(ctx))
|
||||
}
|
||||
|
||||
func (r *GroupRepository) InvitationCreate(ctx context.Context, groupID uuid.UUID, invite GroupInvitationCreate) (GroupInvitation, error) {
|
||||
entity, err := r.db.GroupInvitationToken.Create().
|
||||
SetGroupID(groupID).
|
||||
SetToken(invite.Token).
|
||||
SetExpiresAt(invite.ExpiresAt).
|
||||
SetUses(invite.Uses).
|
||||
Save(ctx)
|
||||
|
||||
if err != nil {
|
||||
return GroupInvitation{}, err
|
||||
}
|
||||
|
||||
return r.InvitationGet(ctx, entity.Token)
|
||||
}
|
||||
|
||||
func (r *GroupRepository) InvitationUpdate(ctx context.Context, id uuid.UUID, uses int) error {
|
||||
_, err := r.db.GroupInvitationToken.UpdateOneID(id).SetUses(uses).Save(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// InvitationPurge removes all expired invitations or those that have been used up.
|
||||
// It returns the number of deleted invitations.
|
||||
func (r *GroupRepository) InvitationPurge(ctx context.Context) (amount int, err error) {
|
||||
q := r.db.GroupInvitationToken.Delete()
|
||||
q.Where(groupinvitationtoken.Or(
|
||||
groupinvitationtoken.ExpiresAtLT(time.Now()),
|
||||
groupinvitationtoken.UsesLTE(0),
|
||||
))
|
||||
|
||||
return q.Exec(ctx)
|
||||
}
|
33
backend/internal/data/repo/repo_group_test.go
Normal file
33
backend/internal/data/repo/repo_group_test.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Group_Create(t *testing.T) {
|
||||
g, err := tRepos.Groups.GroupCreate(context.Background(), "test")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test", g.Name)
|
||||
|
||||
// Get by ID
|
||||
foundGroup, err := tRepos.Groups.GroupByID(context.Background(), g.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, g.ID, foundGroup.ID)
|
||||
}
|
||||
|
||||
func Test_Group_Update(t *testing.T) {
|
||||
g, err := tRepos.Groups.GroupCreate(context.Background(), "test")
|
||||
assert.NoError(t, err)
|
||||
|
||||
g, err = tRepos.Groups.GroupUpdate(context.Background(), g.ID, GroupUpdate{
|
||||
Name: "test2",
|
||||
Currency: "eur",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test2", g.Name)
|
||||
assert.Equal(t, "EUR", g.Currency)
|
||||
}
|
81
backend/internal/data/repo/repo_item_attachments.go
Normal file
81
backend/internal/data/repo/repo_item_attachments.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/attachment"
|
||||
)
|
||||
|
||||
// AttachmentRepo is a repository for Attachments table that links Items to Documents
|
||||
// While also specifying the type of the attachment. This _ONLY_ provides basic Create Update
|
||||
// And Delete operations. For accessing the actual documents, use the Items repository since it
|
||||
// provides the attachments with the documents.
|
||||
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).
|
||||
SetDocumentID(docId).
|
||||
SetItemID(itemId).
|
||||
Save(ctx)
|
||||
}
|
||||
|
||||
func (r *AttachmentRepo) Get(ctx context.Context, id uuid.UUID) (*ent.Attachment, error) {
|
||||
return r.db.Attachment.
|
||||
Query().
|
||||
Where(attachment.ID(id)).
|
||||
WithItem().
|
||||
WithDocument().
|
||||
Only(ctx)
|
||||
}
|
||||
|
||||
func (r *AttachmentRepo) Update(ctx context.Context, itemId uuid.UUID, typ attachment.Type) (*ent.Attachment, error) {
|
||||
itm, err := r.db.Attachment.UpdateOneID(itemId).
|
||||
SetType(typ).
|
||||
Save(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.Get(ctx, itm.ID)
|
||||
}
|
||||
|
||||
func (r *AttachmentRepo) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return r.db.Attachment.DeleteOneID(id).Exec(ctx)
|
||||
}
|
133
backend/internal/data/repo/repo_item_attachments_test.go
Normal file
133
backend/internal/data/repo/repo_item_attachments_test.go
Normal file
|
@ -0,0 +1,133 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/attachment"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAttachmentRepo_Create(t *testing.T) {
|
||||
doc := useDocs(t, 1)[0]
|
||||
item := useItems(t, 1)[0]
|
||||
|
||||
ids := []uuid.UUID{doc.ID, item.ID}
|
||||
t.Cleanup(func() {
|
||||
for _, id := range ids {
|
||||
_ = tRepos.Attachments.Delete(context.Background(), id)
|
||||
}
|
||||
})
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
itemId uuid.UUID
|
||||
docId uuid.UUID
|
||||
typ attachment.Type
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *ent.Attachment
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "create attachment",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
itemId: item.ID,
|
||||
docId: doc.ID,
|
||||
typ: attachment.TypePhoto,
|
||||
},
|
||||
want: &ent.Attachment{
|
||||
Type: attachment.TypePhoto,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create attachment with invalid item id",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
itemId: uuid.New(),
|
||||
docId: doc.ID,
|
||||
typ: "blarg",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
got, err := tRepos.Attachments.Create(tt.args.ctx, tt.args.itemId, tt.args.docId, tt.args.typ)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("AttachmentRepo.Create() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if tt.wantErr {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.want.Type, got.Type)
|
||||
|
||||
withItems, err := tRepos.Attachments.Get(tt.args.ctx, got.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.args.itemId, withItems.Edges.Item.ID)
|
||||
assert.Equal(t, tt.args.docId, withItems.Edges.Document.ID)
|
||||
|
||||
ids = append(ids, got.ID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func useAttachments(t *testing.T, n int) []*ent.Attachment {
|
||||
t.Helper()
|
||||
|
||||
doc := useDocs(t, 1)[0]
|
||||
item := useItems(t, 1)[0]
|
||||
|
||||
ids := make([]uuid.UUID, 0, n)
|
||||
t.Cleanup(func() {
|
||||
for _, id := range ids {
|
||||
_ = tRepos.Attachments.Delete(context.Background(), id)
|
||||
}
|
||||
})
|
||||
|
||||
attachments := make([]*ent.Attachment, n)
|
||||
for i := 0; i < n; i++ {
|
||||
attachment, err := tRepos.Attachments.Create(context.Background(), item.ID, doc.ID, attachment.TypePhoto)
|
||||
assert.NoError(t, err)
|
||||
attachments[i] = attachment
|
||||
|
||||
ids = append(ids, attachment.ID)
|
||||
}
|
||||
|
||||
return attachments
|
||||
}
|
||||
|
||||
func TestAttachmentRepo_Update(t *testing.T) {
|
||||
entity := useAttachments(t, 1)[0]
|
||||
|
||||
for _, typ := range []attachment.Type{"photo", "manual", "warranty", "attachment"} {
|
||||
t.Run(string(typ), func(t *testing.T) {
|
||||
_, err := tRepos.Attachments.Update(context.Background(), entity.ID, typ)
|
||||
assert.NoError(t, err)
|
||||
|
||||
updated, err := tRepos.Attachments.Get(context.Background(), entity.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, typ, updated.Type)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAttachmentRepo_Delete(t *testing.T) {
|
||||
entity := useAttachments(t, 1)[0]
|
||||
|
||||
err := tRepos.Attachments.Delete(context.Background(), entity.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = tRepos.Attachments.Get(context.Background(), entity.ID)
|
||||
assert.Error(t, err)
|
||||
}
|
490
backend/internal/data/repo/repo_items.go
Normal file
490
backend/internal/data/repo/repo_items.go
Normal file
|
@ -0,0 +1,490 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/group"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/item"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/itemfield"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/label"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/location"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/predicate"
|
||||
)
|
||||
|
||||
type ItemsRepository struct {
|
||||
db *ent.Client
|
||||
}
|
||||
|
||||
type (
|
||||
ItemQuery struct {
|
||||
Page int
|
||||
PageSize int
|
||||
Search string `json:"search"`
|
||||
LocationIDs []uuid.UUID `json:"locationIds"`
|
||||
LabelIDs []uuid.UUID `json:"labelIds"`
|
||||
SortBy string `json:"sortBy"`
|
||||
}
|
||||
|
||||
ItemField struct {
|
||||
ID uuid.UUID `json:"id,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
TextValue string `json:"textValue"`
|
||||
NumberValue int `json:"numberValue"`
|
||||
BooleanValue bool `json:"booleanValue"`
|
||||
TimeValue time.Time `json:"timeValue,omitempty"`
|
||||
}
|
||||
|
||||
ItemCreate struct {
|
||||
ImportRef string `json:"-"`
|
||||
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
|
||||
// Edges
|
||||
LocationID uuid.UUID `json:"locationId"`
|
||||
LabelIDs []uuid.UUID `json:"labelIds"`
|
||||
}
|
||||
ItemUpdate struct {
|
||||
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable,x-omitempty"`
|
||||
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 []ItemField `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,omitempty" extensions:"x-nullable,x-omitempty"`
|
||||
Labels []LabelSummary `json:"labels"`
|
||||
}
|
||||
|
||||
ItemOut struct {
|
||||
Parent *ItemSummary `json:"parent,omitempty" extensions:"x-nullable,x-omitempty"`
|
||||
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"`
|
||||
Fields []ItemField `json:"fields"`
|
||||
Children []ItemSummary `json:"children"`
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
mapItemsSummaryErr = mapTEachErrFunc(mapItemSummary)
|
||||
)
|
||||
|
||||
func mapItemSummary(item *ent.Item) ItemSummary {
|
||||
var location *LocationSummary
|
||||
if item.Edges.Location != nil {
|
||||
loc := mapLocationSummary(item.Edges.Location)
|
||||
location = &loc
|
||||
}
|
||||
|
||||
labels := make([]LabelSummary, len(item.Edges.Label))
|
||||
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 mapFields(fields []*ent.ItemField) []ItemField {
|
||||
result := make([]ItemField, len(fields))
|
||||
for i, f := range fields {
|
||||
result[i] = ItemField{
|
||||
ID: f.ID,
|
||||
Type: f.Type.String(),
|
||||
Name: f.Name,
|
||||
TextValue: f.TextValue,
|
||||
NumberValue: f.NumberValue,
|
||||
BooleanValue: f.BooleanValue,
|
||||
TimeValue: f.TimeValue,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mapItemOut(item *ent.Item) ItemOut {
|
||||
var attachments []ItemAttachment
|
||||
if item.Edges.Attachments != nil {
|
||||
attachments = mapEach(item.Edges.Attachments, ToItemAttachment)
|
||||
}
|
||||
|
||||
var fields []ItemField
|
||||
if item.Edges.Fields != nil {
|
||||
fields = mapFields(item.Edges.Fields)
|
||||
}
|
||||
|
||||
var children []ItemSummary
|
||||
if item.Edges.Children != nil {
|
||||
children = mapEach(item.Edges.Children, mapItemSummary)
|
||||
}
|
||||
|
||||
var parent *ItemSummary
|
||||
if item.Edges.Parent != nil {
|
||||
v := mapItemSummary(item.Edges.Parent)
|
||||
parent = &v
|
||||
}
|
||||
|
||||
return ItemOut{
|
||||
Parent: parent,
|
||||
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,
|
||||
Fields: fields,
|
||||
Children: children,
|
||||
}
|
||||
}
|
||||
|
||||
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().
|
||||
WithGroup().
|
||||
WithChildren().
|
||||
WithParent().
|
||||
WithAttachments(func(aq *ent.AttachmentQuery) {
|
||||
aq.WithDocument()
|
||||
}).
|
||||
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))
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) CheckRef(ctx context.Context, GID uuid.UUID, ref string) (bool, error) {
|
||||
q := e.db.Item.Query().Where(item.HasGroupWith(group.ID(GID)))
|
||||
return q.Where(item.ImportRef(ref)).Exist(ctx)
|
||||
}
|
||||
|
||||
// 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)))
|
||||
}
|
||||
|
||||
// QueryByGroup returns a list of items that belong to a specific group based on the provided query.
|
||||
func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q ItemQuery) (PaginationResult[ItemSummary], error) {
|
||||
qb := e.db.Item.Query().Where(item.HasGroupWith(group.ID(gid)))
|
||||
|
||||
if len(q.LabelIDs) > 0 {
|
||||
labels := make([]predicate.Item, 0, len(q.LabelIDs))
|
||||
for _, l := range q.LabelIDs {
|
||||
labels = append(labels, item.HasLabelWith(label.ID(l)))
|
||||
}
|
||||
qb = qb.Where(item.Or(labels...))
|
||||
}
|
||||
|
||||
if len(q.LocationIDs) > 0 {
|
||||
locations := make([]predicate.Item, 0, len(q.LocationIDs))
|
||||
for _, l := range q.LocationIDs {
|
||||
locations = append(locations, item.HasLocationWith(location.ID(l)))
|
||||
}
|
||||
qb = qb.Where(item.Or(locations...))
|
||||
}
|
||||
|
||||
if q.Search != "" {
|
||||
qb.Where(
|
||||
item.Or(
|
||||
item.NameContainsFold(q.Search),
|
||||
item.DescriptionContainsFold(q.Search),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if q.Page != -1 || q.PageSize != -1 {
|
||||
qb = qb.
|
||||
Offset(calculateOffset(q.Page, q.PageSize)).
|
||||
Limit(q.PageSize)
|
||||
}
|
||||
|
||||
items, err := mapItemsSummaryErr(
|
||||
qb.Order(ent.Asc(item.FieldName)).
|
||||
WithLabel().
|
||||
WithLocation().
|
||||
All(ctx),
|
||||
)
|
||||
if err != nil {
|
||||
return PaginationResult[ItemSummary]{}, err
|
||||
}
|
||||
|
||||
count, err := qb.Count(ctx)
|
||||
if err != nil {
|
||||
return PaginationResult[ItemSummary]{}, err
|
||||
}
|
||||
|
||||
return PaginationResult[ItemSummary]{
|
||||
Page: q.Page,
|
||||
PageSize: q.PageSize,
|
||||
Total: count,
|
||||
Items: items,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
// 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) ([]ItemSummary, error) {
|
||||
return mapItemsSummaryErr(e.db.Item.Query().
|
||||
Where(item.HasGroupWith(group.ID(gid))).
|
||||
WithLabel().
|
||||
WithLocation().
|
||||
All(ctx))
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) Create(ctx context.Context, gid uuid.UUID, data ItemCreate) (ItemOut, error) {
|
||||
q := e.db.Item.Create().
|
||||
SetImportRef(data.ImportRef).
|
||||
SetName(data.Name).
|
||||
SetDescription(data.Description).
|
||||
SetGroupID(gid).
|
||||
SetLocationID(data.LocationID)
|
||||
|
||||
if data.LabelIDs != nil && len(data.LabelIDs) > 0 {
|
||||
q.AddLabelIDs(data.LabelIDs...)
|
||||
}
|
||||
|
||||
result, err := q.Save(ctx)
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
|
||||
return e.GetOne(ctx, result.ID)
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return e.db.Item.DeleteOneID(id).Exec(ctx)
|
||||
}
|
||||
|
||||
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).
|
||||
SetSerialNumber(data.SerialNumber).
|
||||
SetModelNumber(data.ModelNumber).
|
||||
SetManufacturer(data.Manufacturer).
|
||||
SetPurchaseTime(data.PurchaseTime).
|
||||
SetPurchaseFrom(data.PurchaseFrom).
|
||||
SetPurchasePrice(data.PurchasePrice).
|
||||
SetSoldTime(data.SoldTime).
|
||||
SetSoldTo(data.SoldTo).
|
||||
SetSoldPrice(data.SoldPrice).
|
||||
SetSoldNotes(data.SoldNotes).
|
||||
SetNotes(data.Notes).
|
||||
SetLifetimeWarranty(data.LifetimeWarranty).
|
||||
SetInsured(data.Insured).
|
||||
SetWarrantyExpires(data.WarrantyExpires).
|
||||
SetWarrantyDetails(data.WarrantyDetails).
|
||||
SetQuantity(data.Quantity)
|
||||
|
||||
currentLabels, err := e.db.Item.Query().Where(item.ID(data.ID)).QueryLabel().All(ctx)
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
|
||||
set := newIDSet(currentLabels)
|
||||
|
||||
for _, l := range data.LabelIDs {
|
||||
if set.Contains(l) {
|
||||
set.Remove(l)
|
||||
continue
|
||||
}
|
||||
q.AddLabelIDs(l)
|
||||
}
|
||||
|
||||
if set.Len() > 0 {
|
||||
q.RemoveLabelIDs(set.Slice()...)
|
||||
}
|
||||
|
||||
if data.ParentID != uuid.Nil {
|
||||
q.SetParentID(data.ParentID)
|
||||
} else {
|
||||
q.ClearParent()
|
||||
}
|
||||
|
||||
err = q.Exec(ctx)
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
|
||||
fields, err := e.db.ItemField.Query().Where(itemfield.HasItemWith(item.ID(data.ID))).All(ctx)
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
|
||||
fieldIds := newIDSet(fields)
|
||||
|
||||
// Update Existing Fields
|
||||
for _, f := range data.Fields {
|
||||
if f.ID == uuid.Nil {
|
||||
// Create New Field
|
||||
_, err = e.db.ItemField.Create().
|
||||
SetItemID(data.ID).
|
||||
SetType(itemfield.Type(f.Type)).
|
||||
SetName(f.Name).
|
||||
SetTextValue(f.TextValue).
|
||||
SetNumberValue(f.NumberValue).
|
||||
SetBooleanValue(f.BooleanValue).
|
||||
SetTimeValue(f.TimeValue).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
}
|
||||
|
||||
opt := e.db.ItemField.Update().
|
||||
Where(
|
||||
itemfield.ID(f.ID),
|
||||
itemfield.HasItemWith(item.ID(data.ID)),
|
||||
).
|
||||
SetType(itemfield.Type(f.Type)).
|
||||
SetName(f.Name).
|
||||
SetTextValue(f.TextValue).
|
||||
SetNumberValue(f.NumberValue).
|
||||
SetBooleanValue(f.BooleanValue).
|
||||
SetTimeValue(f.TimeValue)
|
||||
|
||||
_, err = opt.Save(ctx)
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
|
||||
fieldIds.Remove(f.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Delete Fields that are no longer present
|
||||
if fieldIds.Len() > 0 {
|
||||
_, err = e.db.ItemField.Delete().
|
||||
Where(
|
||||
itemfield.IDIn(fieldIds.Slice()...),
|
||||
itemfield.HasItemWith(item.ID(data.ID)),
|
||||
).Exec(ctx)
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return e.GetOne(ctx, data.ID)
|
||||
}
|
273
backend/internal/data/repo/repo_items_test.go
Normal file
273
backend/internal/data/repo/repo_items_test.go
Normal file
|
@ -0,0 +1,273 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func itemFactory() ItemCreate {
|
||||
return ItemCreate{
|
||||
Name: fk.Str(10),
|
||||
Description: fk.Str(100),
|
||||
}
|
||||
}
|
||||
|
||||
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([]ItemOut, len)
|
||||
for i := 0; i < len; i++ {
|
||||
itm := itemFactory()
|
||||
itm.LocationID = location.ID
|
||||
|
||||
item, err := tRepos.Items.Create(context.Background(), tGroup.ID, itm)
|
||||
assert.NoError(t, err)
|
||||
items[i] = item
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
for _, item := range items {
|
||||
_ = tRepos.Items.Delete(context.Background(), item.ID)
|
||||
}
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func TestItemsRepository_RecursiveRelationships(t *testing.T) {
|
||||
parent := useItems(t, 1)[0]
|
||||
|
||||
children := useItems(t, 3)
|
||||
|
||||
for _, child := range children {
|
||||
update := ItemUpdate{
|
||||
ID: child.ID,
|
||||
ParentID: parent.ID,
|
||||
Name: "note-important",
|
||||
Description: "This is a note",
|
||||
LocationID: child.Location.ID,
|
||||
}
|
||||
|
||||
// Append Parent ID
|
||||
_, err := tRepos.Items.UpdateByGroup(context.Background(), tGroup.ID, update)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check Parent ID
|
||||
updated, err := tRepos.Items.GetOne(context.Background(), child.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, parent.ID, updated.Parent.ID)
|
||||
|
||||
// Remove Parent ID
|
||||
update.ParentID = uuid.Nil
|
||||
_, err = tRepos.Items.UpdateByGroup(context.Background(), tGroup.ID, update)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check Parent ID
|
||||
updated, err = tRepos.Items.GetOne(context.Background(), child.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, updated.Parent)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemsRepository_GetOne(t *testing.T) {
|
||||
entity := useItems(t, 3)
|
||||
|
||||
for _, item := range entity {
|
||||
result, err := tRepos.Items.GetOne(context.Background(), item.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, item.ID, result.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemsRepository_GetAll(t *testing.T) {
|
||||
length := 10
|
||||
expected := useItems(t, length)
|
||||
|
||||
results, err := tRepos.Items.GetAll(context.Background(), tGroup.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, length, len(results))
|
||||
|
||||
for _, item := range results {
|
||||
for _, expectedItem := range expected {
|
||||
if item.ID == expectedItem.ID {
|
||||
assert.Equal(t, expectedItem.ID, item.ID)
|
||||
assert.Equal(t, expectedItem.Name, item.Name)
|
||||
assert.Equal(t, expectedItem.Description, item.Description)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemsRepository_Create(t *testing.T) {
|
||||
location, err := tRepos.Locations.Create(context.Background(), tGroup.ID, locationFactory())
|
||||
assert.NoError(t, err)
|
||||
|
||||
itm := itemFactory()
|
||||
itm.LocationID = location.ID
|
||||
|
||||
result, err := tRepos.Items.Create(context.Background(), tGroup.ID, itm)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, result.ID)
|
||||
|
||||
// Cleanup - Also deletes item
|
||||
err = tRepos.Locations.Delete(context.Background(), location.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
}
|
||||
|
||||
func TestItemsRepository_Create_Location(t *testing.T) {
|
||||
location, err := tRepos.Locations.Create(context.Background(), tGroup.ID, locationFactory())
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, location.ID)
|
||||
|
||||
item := itemFactory()
|
||||
item.LocationID = location.ID
|
||||
|
||||
// Create Resource
|
||||
result, err := tRepos.Items.Create(context.Background(), tGroup.ID, item)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, result.ID)
|
||||
|
||||
// Get Resource
|
||||
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.Location.ID)
|
||||
|
||||
// Cleanup - Also deletes item
|
||||
err = tRepos.Locations.Delete(context.Background(), location.ID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestItemsRepository_Delete(t *testing.T) {
|
||||
entities := useItems(t, 3)
|
||||
|
||||
for _, item := range entities {
|
||||
err := tRepos.Items.Delete(context.Background(), item.ID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
results, err := tRepos.Items.GetAll(context.Background(), tGroup.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, results)
|
||||
}
|
||||
|
||||
func TestItemsRepository_Update_Labels(t *testing.T) {
|
||||
entity := useItems(t, 1)[0]
|
||||
labels := useLabels(t, 3)
|
||||
|
||||
labelsIDs := []uuid.UUID{labels[0].ID, labels[1].ID, labels[2].ID}
|
||||
|
||||
type args struct {
|
||||
labelIds []uuid.UUID
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []uuid.UUID
|
||||
}{
|
||||
{
|
||||
name: "add all labels",
|
||||
args: args{
|
||||
labelIds: labelsIDs,
|
||||
},
|
||||
want: labelsIDs,
|
||||
},
|
||||
{
|
||||
name: "update with one label",
|
||||
args: args{
|
||||
labelIds: labelsIDs[:1],
|
||||
},
|
||||
want: labelsIDs[:1],
|
||||
},
|
||||
{
|
||||
name: "add one new label to existing single label",
|
||||
args: args{
|
||||
labelIds: labelsIDs[1:],
|
||||
},
|
||||
want: labelsIDs[1:],
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Apply all labels to entity
|
||||
updateData := ItemUpdate{
|
||||
ID: entity.ID,
|
||||
Name: entity.Name,
|
||||
LocationID: entity.Location.ID,
|
||||
LabelIDs: tt.args.labelIds,
|
||||
}
|
||||
|
||||
updated, err := tRepos.Items.UpdateByGroup(context.Background(), tGroup.ID, updateData)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, tt.want, len(updated.Labels))
|
||||
|
||||
for _, label := range updated.Labels {
|
||||
assert.Contains(t, tt.want, label.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestItemsRepository_Update(t *testing.T) {
|
||||
entities := useItems(t, 3)
|
||||
|
||||
entity := entities[0]
|
||||
|
||||
updateData := ItemUpdate{
|
||||
ID: entity.ID,
|
||||
Name: entity.Name,
|
||||
LocationID: entity.Location.ID,
|
||||
SerialNumber: fk.Str(10),
|
||||
LabelIDs: nil,
|
||||
ModelNumber: fk.Str(10),
|
||||
Manufacturer: fk.Str(10),
|
||||
PurchaseTime: time.Now(),
|
||||
PurchaseFrom: fk.Str(10),
|
||||
PurchasePrice: 300.99,
|
||||
SoldTime: time.Now(),
|
||||
SoldTo: fk.Str(10),
|
||||
SoldPrice: 300.99,
|
||||
SoldNotes: fk.Str(10),
|
||||
Notes: fk.Str(10),
|
||||
WarrantyExpires: time.Now(),
|
||||
WarrantyDetails: fk.Str(10),
|
||||
LifetimeWarranty: true,
|
||||
}
|
||||
|
||||
updatedEntity, err := tRepos.Items.UpdateByGroup(context.Background(), tGroup.ID, updateData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
got, err := tRepos.Items.GetOne(context.Background(), updatedEntity.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, updateData.ID, got.ID)
|
||||
assert.Equal(t, updateData.Name, got.Name)
|
||||
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)
|
||||
// assert.Equal(t, updateData.PurchaseTime, got.PurchaseTime)
|
||||
assert.Equal(t, updateData.PurchaseFrom, got.PurchaseFrom)
|
||||
assert.Equal(t, updateData.PurchasePrice, got.PurchasePrice)
|
||||
// assert.Equal(t, updateData.SoldTime, got.SoldTime)
|
||||
assert.Equal(t, updateData.SoldTo, got.SoldTo)
|
||||
assert.Equal(t, updateData.SoldPrice, got.SoldPrice)
|
||||
assert.Equal(t, updateData.SoldNotes, got.SoldNotes)
|
||||
assert.Equal(t, updateData.Notes, got.Notes)
|
||||
// assert.Equal(t, updateData.WarrantyExpires, got.WarrantyExpires)
|
||||
assert.Equal(t, updateData.WarrantyDetails, got.WarrantyDetails)
|
||||
assert.Equal(t, updateData.LifetimeWarranty, got.LifetimeWarranty)
|
||||
}
|
152
backend/internal/data/repo/repo_labels.go
Normal file
152
backend/internal/data/repo/repo_labels.go
Normal file
|
@ -0,0 +1,152 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/group"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/label"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/predicate"
|
||||
)
|
||||
|
||||
type LabelRepository struct {
|
||||
db *ent.Client
|
||||
}
|
||||
type (
|
||||
LabelCreate struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
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))).
|
||||
Order(ent.Asc(label.FieldName)).
|
||||
WithGroup().
|
||||
All(ctx),
|
||||
)
|
||||
}
|
||||
|
||||
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).
|
||||
SetColor(data.Color).
|
||||
SetGroupID(groupdId).
|
||||
Save(ctx)
|
||||
|
||||
if err != nil {
|
||||
return LabelOut{}, err
|
||||
}
|
||||
|
||||
label.Edges.Group = &ent.Group{ID: groupdId} // bootstrap group ID
|
||||
return mapLabelOut(label), err
|
||||
}
|
||||
|
||||
func (r *LabelRepository) update(ctx context.Context, data LabelUpdate, where ...predicate.Label) (int, error) {
|
||||
if len(where) == 0 {
|
||||
panic("empty where not supported empty")
|
||||
}
|
||||
|
||||
return r.db.Label.Update().
|
||||
Where(where...).
|
||||
SetName(data.Name).
|
||||
SetDescription(data.Description).
|
||||
SetColor(data.Color).
|
||||
Save(ctx)
|
||||
}
|
||||
|
||||
func (r *LabelRepository) Update(ctx context.Context, data LabelUpdate) (LabelOut, error) {
|
||||
_, err := r.update(ctx, data, label.ID(data.ID))
|
||||
if err != nil {
|
||||
return LabelOut{}, err
|
||||
}
|
||||
|
||||
return r.GetOne(ctx, data.ID)
|
||||
}
|
||||
|
||||
func (r *LabelRepository) UpdateByGroup(ctx context.Context, GID uuid.UUID, data LabelUpdate) (LabelOut, error) {
|
||||
_, err := r.update(ctx, data, label.ID(data.ID), label.HasGroupWith(group.ID(GID)))
|
||||
if err != nil {
|
||||
return LabelOut{}, err
|
||||
}
|
||||
|
||||
return r.GetOne(ctx, data.ID)
|
||||
}
|
||||
|
||||
func (r *LabelRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return r.db.Label.DeleteOneID(id).Exec(ctx)
|
||||
}
|
||||
|
||||
func (r *LabelRepository) DeleteByGroup(ctx context.Context, gid, id uuid.UUID) error {
|
||||
_, err := r.db.Label.Delete().
|
||||
Where(
|
||||
label.ID(id),
|
||||
label.HasGroupWith(group.ID(gid)),
|
||||
).Exec(ctx)
|
||||
|
||||
return err
|
||||
}
|
102
backend/internal/data/repo/repo_labels_test.go
Normal file
102
backend/internal/data/repo/repo_labels_test.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func labelFactory() LabelCreate {
|
||||
return LabelCreate{
|
||||
Name: fk.Str(10),
|
||||
Description: fk.Str(100),
|
||||
}
|
||||
}
|
||||
|
||||
func useLabels(t *testing.T, len int) []LabelOut {
|
||||
t.Helper()
|
||||
|
||||
labels := make([]LabelOut, len)
|
||||
for i := 0; i < len; i++ {
|
||||
itm := labelFactory()
|
||||
|
||||
item, err := tRepos.Labels.Create(context.Background(), tGroup.ID, itm)
|
||||
assert.NoError(t, err)
|
||||
labels[i] = item
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
for _, item := range labels {
|
||||
_ = tRepos.Labels.Delete(context.Background(), item.ID)
|
||||
}
|
||||
})
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
func TestLabelRepository_Get(t *testing.T) {
|
||||
labels := useLabels(t, 1)
|
||||
label := labels[0]
|
||||
|
||||
// Get by ID
|
||||
foundLoc, err := tRepos.Labels.GetOne(context.Background(), label.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, label.ID, foundLoc.ID)
|
||||
}
|
||||
|
||||
func TestLabelRepositoryGetAll(t *testing.T) {
|
||||
useLabels(t, 10)
|
||||
|
||||
all, err := tRepos.Labels.GetAll(context.Background(), tGroup.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, all, 10)
|
||||
}
|
||||
|
||||
func TestLabelRepository_Create(t *testing.T) {
|
||||
loc, err := tRepos.Labels.Create(context.Background(), tGroup.ID, labelFactory())
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get by ID
|
||||
foundLoc, err := tRepos.Labels.GetOne(context.Background(), loc.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, loc.ID, foundLoc.ID)
|
||||
|
||||
err = tRepos.Labels.Delete(context.Background(), loc.ID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLabelRepository_Update(t *testing.T) {
|
||||
loc, err := tRepos.Labels.Create(context.Background(), tGroup.ID, labelFactory())
|
||||
assert.NoError(t, err)
|
||||
|
||||
updateData := LabelUpdate{
|
||||
ID: loc.ID,
|
||||
Name: fk.Str(10),
|
||||
Description: fk.Str(100),
|
||||
}
|
||||
|
||||
update, err := tRepos.Labels.Update(context.Background(), updateData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
foundLoc, err := tRepos.Labels.GetOne(context.Background(), loc.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, update.ID, foundLoc.ID)
|
||||
assert.Equal(t, update.Name, foundLoc.Name)
|
||||
assert.Equal(t, update.Description, foundLoc.Description)
|
||||
|
||||
err = tRepos.Labels.Delete(context.Background(), loc.ID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLabelRepository_Delete(t *testing.T) {
|
||||
loc, err := tRepos.Labels.Create(context.Background(), tGroup.ID, labelFactory())
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = tRepos.Labels.Delete(context.Background(), loc.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = tRepos.Labels.GetOne(context.Background(), loc.ID)
|
||||
assert.Error(t, err)
|
||||
}
|
207
backend/internal/data/repo/repo_locations.go
Normal file
207
backend/internal/data/repo/repo_locations.go
Normal file
|
@ -0,0 +1,207 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/group"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/location"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/predicate"
|
||||
)
|
||||
|
||||
type LocationRepository struct {
|
||||
db *ent.Client
|
||||
}
|
||||
|
||||
type (
|
||||
LocationCreate struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
LocationUpdate struct {
|
||||
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable"`
|
||||
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 {
|
||||
Parent *LocationSummary `json:"parent,omitempty"`
|
||||
LocationSummary
|
||||
Items []ItemSummary `json:"items"`
|
||||
Children []LocationSummary `json:"children"`
|
||||
}
|
||||
)
|
||||
|
||||
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 {
|
||||
var parent *LocationSummary
|
||||
if location.Edges.Parent != nil {
|
||||
p := mapLocationSummary(location.Edges.Parent)
|
||||
parent = &p
|
||||
}
|
||||
|
||||
children := make([]LocationSummary, 0, len(location.Edges.Children))
|
||||
for _, c := range location.Edges.Children {
|
||||
children = append(children, mapLocationSummary(c))
|
||||
}
|
||||
|
||||
return LocationOut{
|
||||
Parent: parent,
|
||||
Children: children,
|
||||
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) ([]LocationOutCount, error) {
|
||||
query := `--sql
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
created_at,
|
||||
updated_at,
|
||||
(
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
items
|
||||
WHERE
|
||||
items.location_items = locations.id
|
||||
) as item_count
|
||||
FROM
|
||||
locations
|
||||
WHERE
|
||||
locations.group_locations = ?
|
||||
ORDER BY
|
||||
locations.name ASC
|
||||
`
|
||||
|
||||
rows, err := r.db.Sql().QueryContext(ctx, query, groupId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := []LocationOutCount{}
|
||||
for rows.Next() {
|
||||
var ct LocationOutCount
|
||||
|
||||
err := rows.Scan(&ct.ID, &ct.Name, &ct.Description, &ct.CreatedAt, &ct.UpdatedAt, &ct.ItemCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list = append(list, ct)
|
||||
}
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
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()
|
||||
}).
|
||||
WithParent().
|
||||
WithChildren().
|
||||
Only(ctx))
|
||||
}
|
||||
|
||||
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(GID).
|
||||
Save(ctx)
|
||||
|
||||
if err != nil {
|
||||
return LocationOut{}, err
|
||||
}
|
||||
|
||||
location.Edges.Group = &ent.Group{ID: GID} // bootstrap group ID
|
||||
return mapLocationOut(location), nil
|
||||
}
|
||||
|
||||
func (r *LocationRepository) update(ctx context.Context, data LocationUpdate, where ...predicate.Location) (LocationOut, error) {
|
||||
q := r.db.Location.Update().
|
||||
Where(where...).
|
||||
SetName(data.Name).
|
||||
SetDescription(data.Description)
|
||||
|
||||
if data.ParentID != uuid.Nil {
|
||||
q.SetParentID(data.ParentID)
|
||||
} else {
|
||||
q.ClearParent()
|
||||
}
|
||||
|
||||
_, err := q.Save(ctx)
|
||||
if err != nil {
|
||||
return LocationOut{}, err
|
||||
}
|
||||
|
||||
return r.Get(ctx, data.ID)
|
||||
}
|
||||
|
||||
func (r *LocationRepository) Update(ctx context.Context, data LocationUpdate) (LocationOut, error) {
|
||||
return r.update(ctx, data, location.ID(data.ID))
|
||||
}
|
||||
|
||||
func (r *LocationRepository) UpdateOneByGroup(ctx context.Context, GID, ID uuid.UUID, data LocationUpdate) (LocationOut, error) {
|
||||
return r.update(ctx, data, location.ID(ID), location.HasGroupWith(group.ID(GID)))
|
||||
}
|
||||
|
||||
func (r *LocationRepository) Delete(ctx context.Context, ID uuid.UUID) error {
|
||||
return r.db.Location.DeleteOneID(ID).Exec(ctx)
|
||||
}
|
||||
|
||||
func (r *LocationRepository) DeleteByGroup(ctx context.Context, GID, ID uuid.UUID) error {
|
||||
_, err := r.db.Location.Delete().Where(location.ID(ID), location.HasGroupWith(group.ID(GID))).Exec(ctx)
|
||||
return err
|
||||
}
|
103
backend/internal/data/repo/repo_locations_test.go
Normal file
103
backend/internal/data/repo/repo_locations_test.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func locationFactory() LocationCreate {
|
||||
return LocationCreate{
|
||||
Name: fk.Str(10),
|
||||
Description: fk.Str(100),
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationRepository_Get(t *testing.T) {
|
||||
loc, err := tRepos.Locations.Create(context.Background(), tGroup.ID, locationFactory())
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get by ID
|
||||
foundLoc, err := tRepos.Locations.Get(context.Background(), loc.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, loc.ID, foundLoc.ID)
|
||||
|
||||
err = tRepos.Locations.Delete(context.Background(), loc.ID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLocationRepositoryGetAllWithCount(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
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, ItemCreate{
|
||||
Name: fk.Str(10),
|
||||
Description: fk.Str(100),
|
||||
LocationID: result.ID,
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
results, err := tRepos.Locations.GetAll(context.Background(), tGroup.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, loc := range results {
|
||||
if loc.ID == result.ID {
|
||||
assert.Equal(t, 1, loc.ItemCount)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestLocationRepository_Create(t *testing.T) {
|
||||
loc, err := tRepos.Locations.Create(context.Background(), tGroup.ID, locationFactory())
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get by ID
|
||||
foundLoc, err := tRepos.Locations.Get(context.Background(), loc.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, loc.ID, foundLoc.ID)
|
||||
|
||||
err = tRepos.Locations.Delete(context.Background(), loc.ID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLocationRepository_Update(t *testing.T) {
|
||||
loc, err := tRepos.Locations.Create(context.Background(), tGroup.ID, locationFactory())
|
||||
assert.NoError(t, err)
|
||||
|
||||
updateData := LocationUpdate{
|
||||
ID: loc.ID,
|
||||
Name: fk.Str(10),
|
||||
Description: fk.Str(100),
|
||||
}
|
||||
|
||||
update, err := tRepos.Locations.Update(context.Background(), updateData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
foundLoc, err := tRepos.Locations.Get(context.Background(), loc.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, update.ID, foundLoc.ID)
|
||||
assert.Equal(t, update.Name, foundLoc.Name)
|
||||
assert.Equal(t, update.Description, foundLoc.Description)
|
||||
|
||||
err = tRepos.Locations.Delete(context.Background(), loc.ID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLocationRepository_Delete(t *testing.T) {
|
||||
loc, err := tRepos.Locations.Create(context.Background(), tGroup.ID, locationFactory())
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = tRepos.Locations.Delete(context.Background(), loc.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = tRepos.Locations.Get(context.Background(), loc.ID)
|
||||
assert.Error(t, err)
|
||||
}
|
93
backend/internal/data/repo/repo_tokens.go
Normal file
93
backend/internal/data/repo/repo_tokens.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/authtokens"
|
||||
)
|
||||
|
||||
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) (UserOut, error) {
|
||||
user, err := r.db.AuthTokens.Query().
|
||||
Where(authtokens.Token(token)).
|
||||
Where(authtokens.ExpiresAtGTE(time.Now())).
|
||||
WithUser().
|
||||
QueryUser().
|
||||
WithGroup().
|
||||
Only(ctx)
|
||||
|
||||
if err != nil {
|
||||
return UserOut{}, err
|
||||
}
|
||||
|
||||
return mapUserOut(user), nil
|
||||
}
|
||||
|
||||
// Creates a token for a user
|
||||
func (r *TokenRepository) CreateToken(ctx context.Context, createToken UserAuthTokenCreate) (UserAuthToken, error) {
|
||||
|
||||
dbToken, err := r.db.AuthTokens.Create().
|
||||
SetToken(createToken.TokenHash).
|
||||
SetUserID(createToken.UserID).
|
||||
SetExpiresAt(createToken.ExpiresAt).
|
||||
Save(ctx)
|
||||
|
||||
if err != nil {
|
||||
return UserAuthToken{}, err
|
||||
}
|
||||
|
||||
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
|
||||
func (r *TokenRepository) DeleteToken(ctx context.Context, token []byte) error {
|
||||
_, err := r.db.AuthTokens.Delete().Where(authtokens.Token(token)).Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// PurgeExpiredTokens removes all expired tokens from the database
|
||||
func (r *TokenRepository) PurgeExpiredTokens(ctx context.Context) (int, error) {
|
||||
tokensDeleted, err := r.db.AuthTokens.Delete().Where(authtokens.ExpiresAtLTE(time.Now())).Exec(ctx)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return tokensDeleted, nil
|
||||
}
|
||||
|
||||
func (r *TokenRepository) DeleteAll(ctx context.Context) (int, error) {
|
||||
amount, err := r.db.AuthTokens.Delete().Exec(ctx)
|
||||
return amount, err
|
||||
}
|
138
backend/internal/data/repo/repo_tokens_test.go
Normal file
138
backend/internal/data/repo/repo_tokens_test.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/homebox/backend/pkgs/hasher"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAuthTokenRepo_CreateToken(t *testing.T) {
|
||||
asrt := assert.New(t)
|
||||
ctx := context.Background()
|
||||
user := userFactory()
|
||||
|
||||
userOut, err := tRepos.Users.Create(ctx, user)
|
||||
asrt.NoError(err)
|
||||
|
||||
expiresAt := time.Now().Add(time.Hour)
|
||||
|
||||
generatedToken := hasher.GenerateToken()
|
||||
|
||||
token, err := tRepos.AuthTokens.CreateToken(ctx, UserAuthTokenCreate{
|
||||
TokenHash: generatedToken.Hash,
|
||||
ExpiresAt: expiresAt,
|
||||
UserID: userOut.ID,
|
||||
})
|
||||
|
||||
asrt.NoError(err)
|
||||
asrt.Equal(userOut.ID, token.UserID)
|
||||
asrt.Equal(expiresAt, token.ExpiresAt)
|
||||
|
||||
// Cleanup
|
||||
asrt.NoError(tRepos.Users.Delete(ctx, userOut.ID))
|
||||
_, err = tRepos.AuthTokens.DeleteAll(ctx)
|
||||
asrt.NoError(err)
|
||||
}
|
||||
|
||||
func TestAuthTokenRepo_DeleteToken(t *testing.T) {
|
||||
asrt := assert.New(t)
|
||||
ctx := context.Background()
|
||||
user := userFactory()
|
||||
|
||||
userOut, err := tRepos.Users.Create(ctx, user)
|
||||
asrt.NoError(err)
|
||||
|
||||
expiresAt := time.Now().Add(time.Hour)
|
||||
|
||||
generatedToken := hasher.GenerateToken()
|
||||
|
||||
_, err = tRepos.AuthTokens.CreateToken(ctx, UserAuthTokenCreate{
|
||||
TokenHash: generatedToken.Hash,
|
||||
ExpiresAt: expiresAt,
|
||||
UserID: userOut.ID,
|
||||
})
|
||||
asrt.NoError(err)
|
||||
|
||||
// Delete token
|
||||
err = tRepos.AuthTokens.DeleteToken(ctx, []byte(generatedToken.Raw))
|
||||
asrt.NoError(err)
|
||||
}
|
||||
|
||||
func TestAuthTokenRepo_GetUserByToken(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
user := userFactory()
|
||||
userOut, _ := tRepos.Users.Create(ctx, user)
|
||||
|
||||
expiresAt := time.Now().Add(time.Hour)
|
||||
generatedToken := hasher.GenerateToken()
|
||||
|
||||
token, err := tRepos.AuthTokens.CreateToken(ctx, UserAuthTokenCreate{
|
||||
TokenHash: generatedToken.Hash,
|
||||
ExpiresAt: expiresAt,
|
||||
UserID: userOut.ID,
|
||||
})
|
||||
|
||||
assert.NoError(err)
|
||||
|
||||
// Get User from token
|
||||
foundUser, err := tRepos.AuthTokens.GetUserFromToken(ctx, token.TokenHash)
|
||||
|
||||
assert.NoError(err)
|
||||
assert.Equal(userOut.ID, foundUser.ID)
|
||||
assert.Equal(userOut.Name, foundUser.Name)
|
||||
assert.Equal(userOut.Email, foundUser.Email)
|
||||
|
||||
// Cleanup
|
||||
assert.NoError(tRepos.Users.Delete(ctx, userOut.ID))
|
||||
_, err = tRepos.AuthTokens.DeleteAll(ctx)
|
||||
assert.NoError(err)
|
||||
}
|
||||
|
||||
func TestAuthTokenRepo_PurgeExpiredTokens(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
user := userFactory()
|
||||
userOut, _ := tRepos.Users.Create(ctx, user)
|
||||
|
||||
createdTokens := []UserAuthToken{}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
expiresAt := time.Now()
|
||||
generatedToken := hasher.GenerateToken()
|
||||
|
||||
createdToken, err := tRepos.AuthTokens.CreateToken(ctx, UserAuthTokenCreate{
|
||||
TokenHash: generatedToken.Hash,
|
||||
ExpiresAt: expiresAt,
|
||||
UserID: userOut.ID,
|
||||
})
|
||||
|
||||
assert.NoError(err)
|
||||
assert.NotNil(createdToken)
|
||||
|
||||
createdTokens = append(createdTokens, createdToken)
|
||||
|
||||
}
|
||||
|
||||
// Purge expired tokens
|
||||
tokensDeleted, err := tRepos.AuthTokens.PurgeExpiredTokens(ctx)
|
||||
|
||||
assert.NoError(err)
|
||||
assert.Equal(5, tokensDeleted)
|
||||
|
||||
// Check if tokens are deleted
|
||||
for _, token := range createdTokens {
|
||||
_, err := tRepos.AuthTokens.GetUserFromToken(ctx, token.TokenHash)
|
||||
assert.Error(err)
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
assert.NoError(tRepos.Users.Delete(ctx, userOut.ID))
|
||||
_, err = tRepos.AuthTokens.DeleteAll(ctx)
|
||||
assert.NoError(err)
|
||||
}
|
136
backend/internal/data/repo/repo_users.go
Normal file
136
backend/internal/data/repo/repo_users.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/ent/user"
|
||||
)
|
||||
|
||||
type UserRepository struct {
|
||||
db *ent.Client
|
||||
}
|
||||
|
||||
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"`
|
||||
IsOwner bool `json:"isOwner"`
|
||||
}
|
||||
|
||||
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:"-"`
|
||||
IsOwner bool `json:"isOwner"`
|
||||
}
|
||||
)
|
||||
|
||||
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,
|
||||
IsOwner: user.Role == "owner",
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
role := user.RoleUser
|
||||
if usr.IsOwner {
|
||||
role = user.RoleOwner
|
||||
}
|
||||
|
||||
entUser, err := e.db.User.
|
||||
Create().
|
||||
SetName(usr.Name).
|
||||
SetEmail(usr.Email).
|
||||
SetPassword(usr.Password).
|
||||
SetIsSuperuser(usr.IsSuperuser).
|
||||
SetGroupID(usr.GroupID).
|
||||
SetRole(role).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
return UserOut{}, err
|
||||
}
|
||||
|
||||
return e.GetOneId(ctx, entUser.ID)
|
||||
}
|
||||
|
||||
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).
|
||||
SetEmail(data.Email)
|
||||
|
||||
_, err := q.Save(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *UserRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := e.db.User.Delete().Where(user.ID(id)).Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *UserRepository) DeleteAll(ctx context.Context) error {
|
||||
_, err := e.db.User.Delete().Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *UserRepository) GetSuperusers(ctx context.Context) ([]*ent.User, error) {
|
||||
users, err := e.db.User.Query().Where(user.IsSuperuser(true)).All(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) ChangePassword(ctx context.Context, UID uuid.UUID, pw string) error {
|
||||
return r.db.User.UpdateOneID(UID).SetPassword(pw).Exec(ctx)
|
||||
}
|
175
backend/internal/data/repo/repo_users_test.go
Normal file
175
backend/internal/data/repo/repo_users_test.go
Normal file
|
@ -0,0 +1,175 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func userFactory() UserCreate {
|
||||
return UserCreate{
|
||||
Name: fk.Str(10),
|
||||
Email: fk.Email(),
|
||||
Password: fk.Str(10),
|
||||
IsSuperuser: fk.Bool(),
|
||||
GroupID: tGroup.ID,
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserRepo_GetOneEmail(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
user := userFactory()
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := tRepos.Users.Create(ctx, user)
|
||||
assert.NoError(err)
|
||||
|
||||
foundUser, err := tRepos.Users.GetOneEmail(ctx, user.Email)
|
||||
|
||||
assert.NotNil(foundUser)
|
||||
assert.Nil(err)
|
||||
assert.Equal(user.Email, foundUser.Email)
|
||||
assert.Equal(user.Name, foundUser.Name)
|
||||
|
||||
// Cleanup
|
||||
err = tRepos.Users.DeleteAll(ctx)
|
||||
assert.NoError(err)
|
||||
}
|
||||
|
||||
func TestUserRepo_GetOneId(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
user := userFactory()
|
||||
ctx := context.Background()
|
||||
|
||||
userOut, _ := tRepos.Users.Create(ctx, user)
|
||||
foundUser, err := tRepos.Users.GetOneId(ctx, userOut.ID)
|
||||
|
||||
assert.NotNil(foundUser)
|
||||
assert.Nil(err)
|
||||
assert.Equal(user.Email, foundUser.Email)
|
||||
assert.Equal(user.Name, foundUser.Name)
|
||||
|
||||
// Cleanup
|
||||
err = tRepos.Users.DeleteAll(ctx)
|
||||
assert.NoError(err)
|
||||
}
|
||||
|
||||
func TestUserRepo_GetAll(t *testing.T) {
|
||||
// Setup
|
||||
toCreate := []UserCreate{
|
||||
userFactory(),
|
||||
userFactory(),
|
||||
userFactory(),
|
||||
userFactory(),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
created := []UserOut{}
|
||||
|
||||
for _, usr := range toCreate {
|
||||
usrOut, _ := tRepos.Users.Create(ctx, usr)
|
||||
created = append(created, usrOut)
|
||||
}
|
||||
|
||||
// Validate
|
||||
allUsers, err := tRepos.Users.GetAll(ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(created), len(allUsers))
|
||||
|
||||
for _, usr := range created {
|
||||
fmt.Printf("%+v\n", usr)
|
||||
for _, usr2 := range allUsers {
|
||||
if usr.ID == usr2.ID {
|
||||
assert.Equal(t, usr.Email, usr2.Email)
|
||||
|
||||
// Check groups are loaded
|
||||
assert.NotNil(t, usr2.GroupID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, usr := range created {
|
||||
_ = tRepos.Users.Delete(ctx, usr.ID)
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
err = tRepos.Users.DeleteAll(ctx)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUserRepo_Update(t *testing.T) {
|
||||
user, err := tRepos.Users.Create(context.Background(), userFactory())
|
||||
assert.NoError(t, err)
|
||||
|
||||
updateData := UserUpdate{
|
||||
Name: fk.Str(10),
|
||||
Email: fk.Email(),
|
||||
}
|
||||
|
||||
// Update
|
||||
err = tRepos.Users.Update(context.Background(), user.ID, updateData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Validate
|
||||
updated, err := tRepos.Users.GetOneId(context.Background(), user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, user.Name, updated.Name)
|
||||
assert.NotEqual(t, user.Email, updated.Email)
|
||||
}
|
||||
|
||||
func TestUserRepo_Delete(t *testing.T) {
|
||||
// Create 10 Users
|
||||
for i := 0; i < 10; i++ {
|
||||
user := userFactory()
|
||||
ctx := context.Background()
|
||||
_, _ = tRepos.Users.Create(ctx, user)
|
||||
}
|
||||
|
||||
// Delete all
|
||||
ctx := context.Background()
|
||||
allUsers, _ := tRepos.Users.GetAll(ctx)
|
||||
|
||||
assert.Greater(t, len(allUsers), 0)
|
||||
err := tRepos.Users.DeleteAll(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
allUsers, _ = tRepos.Users.GetAll(ctx)
|
||||
assert.Equal(t, len(allUsers), 0)
|
||||
|
||||
}
|
||||
|
||||
func TestUserRepo_GetSuperusers(t *testing.T) {
|
||||
// Create 10 Users
|
||||
superuser := 0
|
||||
users := 0
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
user := userFactory()
|
||||
ctx := context.Background()
|
||||
_, _ = tRepos.Users.Create(ctx, user)
|
||||
|
||||
if user.IsSuperuser {
|
||||
superuser++
|
||||
} else {
|
||||
users++
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all
|
||||
ctx := context.Background()
|
||||
|
||||
superUsers, err := tRepos.Users.GetSuperusers(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, usr := range superUsers {
|
||||
assert.True(t, usr.IsSuperuser)
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
err = tRepos.Users.DeleteAll(ctx)
|
||||
assert.NoError(t, err)
|
||||
}
|
30
backend/internal/data/repo/repos_all.go
Normal file
30
backend/internal/data/repo/repos_all.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package repo
|
||||
|
||||
import "github.com/hay-kot/homebox/backend/internal/data/ent"
|
||||
|
||||
// AllRepos is a container for all the repository interfaces
|
||||
type AllRepos struct {
|
||||
Users *UserRepository
|
||||
AuthTokens *TokenRepository
|
||||
Groups *GroupRepository
|
||||
Locations *LocationRepository
|
||||
Labels *LabelRepository
|
||||
Items *ItemsRepository
|
||||
Docs *DocumentRepository
|
||||
DocTokens *DocumentTokensRepository
|
||||
Attachments *AttachmentRepo
|
||||
}
|
||||
|
||||
func New(db *ent.Client, root string) *AllRepos {
|
||||
return &AllRepos{
|
||||
Users: &UserRepository{db},
|
||||
AuthTokens: &TokenRepository{db},
|
||||
Groups: &GroupRepository{db},
|
||||
Locations: &LocationRepository{db},
|
||||
Labels: &LabelRepository{db},
|
||||
Items: &ItemsRepository{db},
|
||||
Docs: &DocumentRepository{db, root},
|
||||
DocTokens: &DocumentTokensRepository{db},
|
||||
Attachments: &AttachmentRepo{db},
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue