forked from mirrors/homebox
feat: items-editor (#5)
* format readme * update logo * format html * add logo to docs * repository for document and document tokens * add attachments type and repository * autogenerate types via scripts * use autogenerated types * attachment type updates * add insured and quantity fields for items * implement HasID interface for entities * implement label updates for items * implement service update method * WIP item update client side actions * check err on attachment * finish types for basic items editor * remove unused var * house keeping
This commit is contained in:
parent
fbc364dcd2
commit
95ab14b866
125 changed files with 15626 additions and 1791 deletions
|
@ -8,9 +8,9 @@ import (
|
|||
func UserFactory() types.UserCreate {
|
||||
f := faker.NewFaker()
|
||||
return types.UserCreate{
|
||||
Name: f.RandomString(10),
|
||||
Email: f.RandomEmail(),
|
||||
Password: f.RandomString(10),
|
||||
IsSuperuser: f.RandomBool(),
|
||||
Name: f.Str(10),
|
||||
Email: f.Email(),
|
||||
Password: f.Str(10),
|
||||
IsSuperuser: f.Bool(),
|
||||
}
|
||||
}
|
||||
|
|
62
backend/internal/repo/id_set.go
Normal file
62
backend/internal/repo/id_set.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package repo
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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))
|
||||
for _, e := range entities {
|
||||
s.Add(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)
|
||||
}
|
47
backend/internal/repo/repo_documents.go
Normal file
47
backend/internal/repo/repo_documents.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/ent/document"
|
||||
"github.com/hay-kot/content/backend/ent/group"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
)
|
||||
|
||||
// DocumentRepository is a repository for Document entity
|
||||
type DocumentRepository struct {
|
||||
db *ent.Client
|
||||
}
|
||||
|
||||
func (r *DocumentRepository) Create(ctx context.Context, gid uuid.UUID, doc types.DocumentCreate) (*ent.Document, error) {
|
||||
return r.db.Document.Create().
|
||||
SetGroupID(gid).
|
||||
SetTitle(doc.Title).
|
||||
SetPath(doc.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) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return r.db.Document.DeleteOneID(id).Exec(ctx)
|
||||
}
|
202
backend/internal/repo/repo_documents_test.go
Normal file
202
backend/internal/repo/repo_documents_test.go
Normal file
|
@ -0,0 +1,202 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/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 {
|
||||
t.Helper()
|
||||
|
||||
results := make([]*ent.Document, 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(),
|
||||
})
|
||||
|
||||
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_GetAll(t *testing.T) {
|
||||
entities := useDocs(t, 10)
|
||||
|
||||
for _, entity := range entities {
|
||||
assert.NotNil(t, entity)
|
||||
}
|
||||
|
||||
all, err := tRepos.Docs.GetAll(context.Background(), tGroup.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
41
backend/internal/repo/repo_documents_tokens.go
Normal file
41
backend/internal/repo/repo_documents_tokens.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/ent/documenttoken"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
)
|
||||
|
||||
// DocumentTokensRepository is a repository for Document entity
|
||||
type DocumentTokensRepository struct {
|
||||
db *ent.Client
|
||||
}
|
||||
|
||||
func (r *DocumentTokensRepository) Create(ctx context.Context, data types.DocumentTokenCreate) (*ent.DocumentToken, error) {
|
||||
result, err := r.db.DocumentToken.Create().
|
||||
SetDocumentID(data.DocumentID).
|
||||
SetToken(data.TokenHash).
|
||||
SetExpiresAt(data.ExpiresAt).
|
||||
Save(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return 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)
|
||||
}
|
149
backend/internal/repo/repo_documents_tokens_test.go
Normal file
149
backend/internal/repo/repo_documents_tokens_test.go
Normal file
|
@ -0,0 +1,149 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/ent/documenttoken"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"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 types.DocumentTokenCreate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *ent.DocumentToken
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "create document token",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: types.DocumentTokenCreate{
|
||||
DocumentID: doc.ID,
|
||||
TokenHash: []byte("token"),
|
||||
ExpiresAt: expires,
|
||||
},
|
||||
},
|
||||
want: &ent.DocumentToken{
|
||||
Edges: ent.DocumentTokenEdges{
|
||||
Document: doc,
|
||||
},
|
||||
Token: []byte("token"),
|
||||
ExpiresAt: expires,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "create document token with empty token",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: types.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: types.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.Token)
|
||||
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)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func useDocTokens(t *testing.T, num int) []*ent.DocumentToken {
|
||||
entity := useDocs(t, 1)[0]
|
||||
|
||||
results := make([]*ent.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(), types.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)
|
||||
}
|
44
backend/internal/repo/repo_item_attachments.go
Normal file
44
backend/internal/repo/repo_item_attachments.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/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
|
||||
}
|
||||
|
||||
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) {
|
||||
return r.db.Attachment.UpdateOneID(itemId).
|
||||
SetType(typ).
|
||||
Save(ctx)
|
||||
}
|
||||
|
||||
func (r *AttachmentRepo) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return r.db.Attachment.DeleteOneID(id).Exec(ctx)
|
||||
}
|
133
backend/internal/repo/repo_item_attachments_test.go
Normal file
133
backend/internal/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/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/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)
|
||||
}
|
|
@ -21,9 +21,13 @@ func (e *ItemsRepository) GetOne(ctx context.Context, id uuid.UUID) (*ent.Item,
|
|||
WithLabel().
|
||||
WithLocation().
|
||||
WithGroup().
|
||||
WithAttachments(func(aq *ent.AttachmentQuery) {
|
||||
aq.WithDocument()
|
||||
}).
|
||||
Only(ctx)
|
||||
}
|
||||
|
||||
// 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().
|
||||
Where(item.HasGroupWith(group.ID(gid))).
|
||||
|
@ -72,11 +76,31 @@ func (e *ItemsRepository) Update(ctx context.Context, data types.ItemUpdate) (*e
|
|||
SetSoldNotes(data.SoldNotes).
|
||||
SetNotes(data.Notes).
|
||||
SetLifetimeWarranty(data.LifetimeWarranty).
|
||||
SetInsured(data.Insured).
|
||||
SetWarrantyExpires(data.WarrantyExpires).
|
||||
SetWarrantyDetails(data.WarrantyDetails)
|
||||
SetWarrantyDetails(data.WarrantyDetails).
|
||||
SetQuantity(data.Quantity)
|
||||
|
||||
err := q.Exec(ctx)
|
||||
currentLabels, err := e.db.Item.Query().Where(item.ID(data.ID)).QueryLabel().All(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
set := EntitiesToIDSet(currentLabels)
|
||||
|
||||
for _, l := range data.LabelIDs {
|
||||
if set.Has(l) {
|
||||
set.Remove(l)
|
||||
continue
|
||||
}
|
||||
q.AddLabelIDs(l)
|
||||
}
|
||||
|
||||
if set.Len() > 0 {
|
||||
q.RemoveLabelIDs(set.Slice()...)
|
||||
}
|
||||
|
||||
err = q.Exec(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -12,12 +13,12 @@ import (
|
|||
|
||||
func itemFactory() types.ItemCreate {
|
||||
return types.ItemCreate{
|
||||
Name: fk.RandomString(10),
|
||||
Description: fk.RandomString(100),
|
||||
Name: fk.Str(10),
|
||||
Description: fk.Str(100),
|
||||
}
|
||||
}
|
||||
|
||||
func useItems(t *testing.T, len int) ([]*ent.Item, func()) {
|
||||
func useItems(t *testing.T, len int) []*ent.Item {
|
||||
t.Helper()
|
||||
|
||||
location, err := tRepos.Locations.Create(context.Background(), tGroup.ID, locationFactory())
|
||||
|
@ -33,17 +34,17 @@ func useItems(t *testing.T, len int) ([]*ent.Item, func()) {
|
|||
items[i] = item
|
||||
}
|
||||
|
||||
return items, func() {
|
||||
t.Cleanup(func() {
|
||||
for _, item := range items {
|
||||
err := tRepos.Items.Delete(context.Background(), item.ID)
|
||||
assert.NoError(t, err)
|
||||
_ = tRepos.Items.Delete(context.Background(), item.ID)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func TestItemsRepository_GetOne(t *testing.T) {
|
||||
entity, cleanup := useItems(t, 3)
|
||||
defer cleanup()
|
||||
entity := useItems(t, 3)
|
||||
|
||||
for _, item := range entity {
|
||||
result, err := tRepos.Items.GetOne(context.Background(), item.ID)
|
||||
|
@ -54,8 +55,7 @@ func TestItemsRepository_GetOne(t *testing.T) {
|
|||
|
||||
func TestItemsRepository_GetAll(t *testing.T) {
|
||||
length := 10
|
||||
expected, cleanup := useItems(t, length)
|
||||
defer cleanup()
|
||||
expected := useItems(t, length)
|
||||
|
||||
results, err := tRepos.Items.GetAll(context.Background(), tGroup.ID)
|
||||
assert.NoError(t, err)
|
||||
|
@ -119,7 +119,7 @@ func TestItemsRepository_Create_Location(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestItemsRepository_Delete(t *testing.T) {
|
||||
entities, _ := useItems(t, 3)
|
||||
entities := useItems(t, 3)
|
||||
|
||||
for _, item := range entities {
|
||||
err := tRepos.Items.Delete(context.Background(), item.ID)
|
||||
|
@ -131,9 +131,68 @@ func TestItemsRepository_Delete(t *testing.T) {
|
|||
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 := types.ItemUpdate{
|
||||
ID: entity.ID,
|
||||
Name: entity.Name,
|
||||
LocationID: entity.Edges.Location.ID,
|
||||
LabelIDs: tt.args.labelIds,
|
||||
}
|
||||
|
||||
updated, err := tRepos.Items.Update(context.Background(), updateData)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, tt.want, len(updated.Edges.Label))
|
||||
|
||||
for _, label := range updated.Edges.Label {
|
||||
assert.Contains(t, tt.want, label.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestItemsRepository_Update(t *testing.T) {
|
||||
entities, cleanup := useItems(t, 3)
|
||||
defer cleanup()
|
||||
entities := useItems(t, 3)
|
||||
|
||||
entity := entities[0]
|
||||
|
||||
|
@ -141,20 +200,20 @@ func TestItemsRepository_Update(t *testing.T) {
|
|||
ID: entity.ID,
|
||||
Name: entity.Name,
|
||||
LocationID: entity.Edges.Location.ID,
|
||||
SerialNumber: fk.RandomString(10),
|
||||
SerialNumber: fk.Str(10),
|
||||
LabelIDs: nil,
|
||||
ModelNumber: fk.RandomString(10),
|
||||
Manufacturer: fk.RandomString(10),
|
||||
ModelNumber: fk.Str(10),
|
||||
Manufacturer: fk.Str(10),
|
||||
PurchaseTime: time.Now(),
|
||||
PurchaseFrom: fk.RandomString(10),
|
||||
PurchaseFrom: fk.Str(10),
|
||||
PurchasePrice: 300.99,
|
||||
SoldTime: time.Now(),
|
||||
SoldTo: fk.RandomString(10),
|
||||
SoldTo: fk.Str(10),
|
||||
SoldPrice: 300.99,
|
||||
SoldNotes: fk.RandomString(10),
|
||||
Notes: fk.RandomString(10),
|
||||
SoldNotes: fk.Str(10),
|
||||
Notes: fk.Str(10),
|
||||
WarrantyExpires: time.Now(),
|
||||
WarrantyDetails: fk.RandomString(10),
|
||||
WarrantyDetails: fk.Str(10),
|
||||
LifetimeWarranty: true,
|
||||
}
|
||||
|
||||
|
|
|
@ -11,12 +11,12 @@ import (
|
|||
|
||||
func labelFactory() types.LabelCreate {
|
||||
return types.LabelCreate{
|
||||
Name: fk.RandomString(10),
|
||||
Description: fk.RandomString(100),
|
||||
Name: fk.Str(10),
|
||||
Description: fk.Str(100),
|
||||
}
|
||||
}
|
||||
|
||||
func useLabels(t *testing.T, len int) ([]*ent.Label, func()) {
|
||||
func useLabels(t *testing.T, len int) []*ent.Label {
|
||||
t.Helper()
|
||||
|
||||
labels := make([]*ent.Label, len)
|
||||
|
@ -28,17 +28,17 @@ func useLabels(t *testing.T, len int) ([]*ent.Label, func()) {
|
|||
labels[i] = item
|
||||
}
|
||||
|
||||
return labels, func() {
|
||||
t.Cleanup(func() {
|
||||
for _, item := range labels {
|
||||
err := tRepos.Labels.Delete(context.Background(), item.ID)
|
||||
assert.NoError(t, err)
|
||||
_ = tRepos.Labels.Delete(context.Background(), item.ID)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
func TestLabelRepository_Get(t *testing.T) {
|
||||
labels, cleanup := useLabels(t, 1)
|
||||
defer cleanup()
|
||||
labels := useLabels(t, 1)
|
||||
label := labels[0]
|
||||
|
||||
// Get by ID
|
||||
|
@ -48,8 +48,7 @@ func TestLabelRepository_Get(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLabelRepositoryGetAll(t *testing.T) {
|
||||
_, cleanup := useLabels(t, 10)
|
||||
defer cleanup()
|
||||
useLabels(t, 10)
|
||||
|
||||
all, err := tRepos.Labels.GetAll(context.Background(), tGroup.ID)
|
||||
assert.NoError(t, err)
|
||||
|
@ -75,8 +74,8 @@ func TestLabelRepository_Update(t *testing.T) {
|
|||
|
||||
updateData := types.LabelUpdate{
|
||||
ID: loc.ID,
|
||||
Name: fk.RandomString(10),
|
||||
Description: fk.RandomString(100),
|
||||
Name: fk.Str(10),
|
||||
Description: fk.Str(100),
|
||||
}
|
||||
|
||||
update, err := tRepos.Labels.Update(context.Background(), updateData)
|
||||
|
|
|
@ -10,8 +10,8 @@ import (
|
|||
|
||||
func locationFactory() types.LocationCreate {
|
||||
return types.LocationCreate{
|
||||
Name: fk.RandomString(10),
|
||||
Description: fk.RandomString(100),
|
||||
Name: fk.Str(10),
|
||||
Description: fk.Str(100),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,14 +31,14 @@ func TestLocationRepository_Get(t *testing.T) {
|
|||
func TestLocationRepositoryGetAllWithCount(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
result, err := tRepos.Locations.Create(ctx, tGroup.ID, types.LocationCreate{
|
||||
Name: fk.RandomString(10),
|
||||
Description: fk.RandomString(100),
|
||||
Name: fk.Str(10),
|
||||
Description: fk.Str(100),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = tRepos.Items.Create(ctx, tGroup.ID, types.ItemCreate{
|
||||
Name: fk.RandomString(10),
|
||||
Description: fk.RandomString(100),
|
||||
Name: fk.Str(10),
|
||||
Description: fk.Str(100),
|
||||
LocationID: result.ID,
|
||||
})
|
||||
|
||||
|
@ -74,8 +74,8 @@ func TestLocationRepository_Update(t *testing.T) {
|
|||
|
||||
updateData := types.LocationUpdate{
|
||||
ID: loc.ID,
|
||||
Name: fk.RandomString(10),
|
||||
Description: fk.RandomString(100),
|
||||
Name: fk.Str(10),
|
||||
Description: fk.Str(100),
|
||||
}
|
||||
|
||||
update, err := tRepos.Locations.Update(context.Background(), updateData)
|
||||
|
|
|
@ -13,10 +13,10 @@ import (
|
|||
func userFactory() types.UserCreate {
|
||||
|
||||
return types.UserCreate{
|
||||
Name: fk.RandomString(10),
|
||||
Email: fk.RandomEmail(),
|
||||
Password: fk.RandomString(10),
|
||||
IsSuperuser: fk.RandomBool(),
|
||||
Name: fk.Str(10),
|
||||
Email: fk.Email(),
|
||||
Password: fk.Str(10),
|
||||
IsSuperuser: fk.Bool(),
|
||||
GroupID: tGroup.ID,
|
||||
}
|
||||
}
|
||||
|
@ -109,8 +109,8 @@ func TestUserRepo_Update(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
updateData := types.UserUpdate{
|
||||
Name: fk.RandomString(10),
|
||||
Email: fk.RandomEmail(),
|
||||
Name: fk.Str(10),
|
||||
Email: fk.Email(),
|
||||
}
|
||||
|
||||
// Update
|
||||
|
|
|
@ -4,21 +4,27 @@ import "github.com/hay-kot/content/backend/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
|
||||
Users *UserRepository
|
||||
AuthTokens *TokenRepository
|
||||
Groups *GroupRepository
|
||||
Locations *LocationRepository
|
||||
Labels *LabelRepository
|
||||
Items *ItemsRepository
|
||||
Docs *DocumentRepository
|
||||
DocTokens *DocumentTokensRepository
|
||||
Attachments *AttachmentRepo
|
||||
}
|
||||
|
||||
func EntAllRepos(db *ent.Client) *AllRepos {
|
||||
return &AllRepos{
|
||||
Users: &UserRepository{db},
|
||||
AuthTokens: &TokenRepository{db},
|
||||
Groups: &GroupRepository{db},
|
||||
Locations: &LocationRepository{db},
|
||||
Labels: &LabelRepository{db},
|
||||
Items: &ItemsRepository{db},
|
||||
Users: &UserRepository{db},
|
||||
AuthTokens: &TokenRepository{db},
|
||||
Groups: &GroupRepository{db},
|
||||
Locations: &LocationRepository{db},
|
||||
Labels: &LabelRepository{db},
|
||||
Items: &ItemsRepository{db},
|
||||
Docs: &DocumentRepository{db},
|
||||
DocTokens: &DocumentTokensRepository{db},
|
||||
Attachments: &AttachmentRepo{db},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,9 @@ func NewServices(repos *repo.AllRepos) *AllServices {
|
|||
Admin: &AdminService{repos},
|
||||
Location: &LocationService{repos},
|
||||
Labels: &LabelService{repos},
|
||||
Items: &ItemService{repos},
|
||||
Items: &ItemService{
|
||||
repo: repos,
|
||||
filepath: "/tmp/content",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ var (
|
|||
tRepos *repo.AllRepos
|
||||
tUser *ent.User
|
||||
tGroup *ent.Group
|
||||
tSvc *AllServices
|
||||
)
|
||||
|
||||
func bootstrap() {
|
||||
|
@ -36,10 +37,10 @@ func bootstrap() {
|
|||
}
|
||||
|
||||
tUser, err = tRepos.Users.Create(ctx, types.UserCreate{
|
||||
Name: fk.RandomString(10),
|
||||
Email: fk.RandomEmail(),
|
||||
Password: fk.RandomString(10),
|
||||
IsSuperuser: fk.RandomBool(),
|
||||
Name: fk.Str(10),
|
||||
Email: fk.Email(),
|
||||
Password: fk.Str(10),
|
||||
IsSuperuser: fk.Bool(),
|
||||
GroupID: tGroup.ID,
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -62,6 +63,7 @@ func TestMain(m *testing.M) {
|
|||
|
||||
tClient = client
|
||||
tRepos = repo.EntAllRepos(tClient)
|
||||
tSvc = NewServices(tRepos)
|
||||
defer client.Close()
|
||||
|
||||
bootstrap()
|
||||
|
|
|
@ -5,6 +5,19 @@ import (
|
|||
"github.com/hay-kot/content/backend/internal/types"
|
||||
)
|
||||
|
||||
func ToItemAttachment(attachment *ent.Attachment) *types.ItemAttachment {
|
||||
return &types.ItemAttachment{
|
||||
ID: attachment.ID,
|
||||
CreatedAt: attachment.CreatedAt,
|
||||
UpdatedAt: attachment.UpdatedAt,
|
||||
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 {
|
||||
|
@ -23,6 +36,14 @@ func ToItemSummary(item *ent.Item) *types.ItemSummary {
|
|||
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,
|
||||
|
@ -53,8 +74,14 @@ func ToItemSummaryErr(item *ent.Item, err error) (*types.ItemSummary, error) {
|
|||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,12 @@ package services
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/ent/attachment"
|
||||
"github.com/hay-kot/content/backend/internal/repo"
|
||||
"github.com/hay-kot/content/backend/internal/services/mappers"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
|
@ -13,6 +17,9 @@ import (
|
|||
|
||||
type ItemService struct {
|
||||
repo *repo.AllRepos
|
||||
|
||||
// filepath is the root of the storage location that will be used to store all files from.
|
||||
filepath string
|
||||
}
|
||||
|
||||
func (svc *ItemService) GetOne(ctx context.Context, gid uuid.UUID, id uuid.UUID) (*types.ItemOut, error) {
|
||||
|
@ -41,6 +48,7 @@ func (svc *ItemService) GetAll(ctx context.Context, gid uuid.UUID) ([]*types.Ite
|
|||
|
||||
return itemsOut, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -49,6 +57,7 @@ func (svc *ItemService) Create(ctx context.Context, gid uuid.UUID, data types.It
|
|||
|
||||
return mappers.ToItemOut(item), nil
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -66,8 +75,76 @@ func (svc *ItemService) Delete(ctx context.Context, gid uuid.UUID, id uuid.UUID)
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *ItemService) Update(ctx context.Context, gid uuid.UUID, data types.ItemUpdate) (*types.ItemOut, error) {
|
||||
panic("implement me")
|
||||
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) attachmentPath(gid, itemId uuid.UUID, filename string) string {
|
||||
return filepath.Join(svc.filepath, gid.String(), itemId.String(), filename)
|
||||
}
|
||||
|
||||
// AddAttachment 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) AddAttachment(ctx context.Context, gid, itemId uuid.UUID, filename string, file io.Reader) (*types.ItemOut, error) {
|
||||
// Get the Item
|
||||
item, err := svc.repo.Items.GetOne(ctx, itemId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if item.Edges.Group.ID != gid {
|
||||
return nil, ErrNotOwner
|
||||
}
|
||||
|
||||
// Create the document
|
||||
doc, err := svc.repo.Docs.Create(ctx, gid, types.DocumentCreate{
|
||||
Title: filename,
|
||||
Path: svc.attachmentPath(gid, itemId, filename),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the attachment
|
||||
_, err = svc.repo.Attachments.Create(ctx, itemId, doc.ID, attachment.TypeAttachment)
|
||||
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
|
||||
}
|
||||
|
||||
return svc.GetOne(ctx, gid, itemId)
|
||||
}
|
||||
|
||||
func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]string) error {
|
||||
|
|
|
@ -2,13 +2,16 @@ package services
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/content/backend/internal/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
||||
|
||||
func TestItemService_CsvImport(t *testing.T) {
|
||||
data := loadcsv()
|
||||
svc := &ItemService{
|
||||
|
@ -55,6 +58,14 @@ func TestItemService_CsvImport(t *testing.T) {
|
|||
labelNames = append(labelNames, label.Name)
|
||||
}
|
||||
|
||||
ids := []uuid.UUID{}
|
||||
t.Cleanup((func() {
|
||||
for _, id := range ids {
|
||||
err := svc.repo.Items.Delete(context.Background(), id)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}))
|
||||
|
||||
for _, item := range items {
|
||||
assert.Contains(t, locNames, item.Location.Name)
|
||||
for _, label := range item.Labels {
|
||||
|
@ -79,6 +90,55 @@ func TestItemService_CsvImport(t *testing.T) {
|
|||
assert.Equal(t, csvRow.parsedSoldPrice(), item.SoldPrice)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemService_AddAttachment(t *testing.T) {
|
||||
temp := os.TempDir()
|
||||
|
||||
svc := &ItemService{
|
||||
repo: tRepos,
|
||||
filepath: temp,
|
||||
}
|
||||
|
||||
loc, err := tSvc.Location.Create(context.Background(), tGroup.ID, types.LocationCreate{
|
||||
Description: "test",
|
||||
Name: "test",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, loc)
|
||||
|
||||
itmC := types.ItemCreate{
|
||||
Name: fk.Str(10),
|
||||
Description: fk.Str(10),
|
||||
LocationID: loc.ID,
|
||||
}
|
||||
|
||||
itm, err := svc.Create(context.Background(), tGroup.ID, itmC)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, itm)
|
||||
t.Cleanup(func() {
|
||||
err := svc.repo.Items.Delete(context.Background(), itm.ID)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
contents := fk.Str(1000)
|
||||
reader := strings.NewReader(contents)
|
||||
|
||||
// Setup
|
||||
afterAttachment, err := svc.AddAttachment(context.Background(), tGroup.ID, itm.ID, "testfile.txt", reader)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, afterAttachment)
|
||||
|
||||
// Check that the file exists
|
||||
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)
|
||||
|
||||
// Check that the file contents are correct
|
||||
bts, err := os.ReadFile(storedPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, contents, string(bts))
|
||||
|
||||
}
|
||||
|
|
31
backend/internal/types/document_types.go
Normal file
31
backend/internal/types/document_types.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
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"`
|
||||
}
|
|
@ -19,6 +19,8 @@ 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"`
|
||||
|
@ -37,12 +39,12 @@ type ItemUpdate struct {
|
|||
// Purchase
|
||||
PurchaseTime time.Time `json:"purchaseTime"`
|
||||
PurchaseFrom string `json:"purchaseFrom"`
|
||||
PurchasePrice float64 `json:"purchasePrice"`
|
||||
PurchasePrice float64 `json:"purchasePrice,string"`
|
||||
|
||||
// Sold
|
||||
SoldTime time.Time `json:"soldTime"`
|
||||
SoldTo string `json:"soldTo"`
|
||||
SoldPrice float64 `json:"soldPrice"`
|
||||
SoldPrice float64 `json:"soldPrice,string"`
|
||||
SoldNotes string `json:"soldNotes"`
|
||||
|
||||
// Extras
|
||||
|
@ -56,6 +58,8 @@ type ItemSummary struct {
|
|||
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"`
|
||||
|
@ -74,12 +78,12 @@ type ItemSummary struct {
|
|||
// Purchase
|
||||
PurchaseTime time.Time `json:"purchaseTime"`
|
||||
PurchaseFrom string `json:"purchaseFrom"`
|
||||
PurchasePrice float64 `json:"purchasePrice"`
|
||||
PurchasePrice float64 `json:"purchasePrice,string"`
|
||||
|
||||
// Sold
|
||||
SoldTime time.Time `json:"soldTime"`
|
||||
SoldTo string `json:"soldTo"`
|
||||
SoldPrice float64 `json:"soldPrice"`
|
||||
SoldPrice float64 `json:"soldPrice,string"`
|
||||
SoldNotes string `json:"soldNotes"`
|
||||
|
||||
// Extras
|
||||
|
@ -88,6 +92,14 @@ type ItemSummary struct {
|
|||
|
||||
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"`
|
||||
Document DocumentOut `json:"document"`
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue