mirror of
https://github.com/hay-kot/homebox.git
synced 2025-07-22 02:20:29 +00:00
feat: maintenance log (#170)
* remove repo for document tokens * remove schema for doc tokens * fix id template and generate cmd * schema updates * code gen * bump dependencies * fix broken migrations + add maintenance entry type * spelling * remove debug logger * implement repository layer * routes * API client * wip: maintenance log * remove depreciated call
This commit is contained in:
parent
d6da63187b
commit
5bbb969763
79 changed files with 6320 additions and 4957 deletions
|
@ -16,17 +16,16 @@ func mapTErrFunc[T any, Y any](fn func(T) Y) func(T, error) (Y, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
// }
|
||||
func mapTEachFunc[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
|
||||
// }
|
||||
// }
|
||||
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) {
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
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)
|
||||
}
|
136
backend/internal/data/repo/repo_maintenance_entry.go
Normal file
136
backend/internal/data/repo/repo_maintenance_entry.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
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/maintenanceentry"
|
||||
)
|
||||
|
||||
// MaintenanceEntryRepository is a repository for maintenance entries that are
|
||||
// associated with an item in the database. An entry represents a maintenance event
|
||||
// that has been performed on an item.
|
||||
type MaintenanceEntryRepository struct {
|
||||
db *ent.Client
|
||||
}
|
||||
type (
|
||||
MaintenanceEntryCreate struct {
|
||||
Date time.Time `json:"date"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Cost float64 `json:"cost,string"`
|
||||
}
|
||||
|
||||
MaintenanceEntry struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Date time.Time `json:"date"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Cost float64 `json:"cost,string"`
|
||||
}
|
||||
|
||||
MaintenanceEntryUpdate struct {
|
||||
Date time.Time `json:"date"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Cost float64 `json:"cost,string"`
|
||||
}
|
||||
|
||||
MaintenanceLog struct {
|
||||
ItemID uuid.UUID `json:"itemId"`
|
||||
CostAverage float64 `json:"costAverage"`
|
||||
CostTotal float64 `json:"costTotal"`
|
||||
Entries []MaintenanceEntry `json:"entries"`
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
mapMaintenanceEntryErr = mapTErrFunc(mapMaintenanceEntry)
|
||||
mapEachMaintenanceEntry = mapTEachFunc(mapMaintenanceEntry)
|
||||
)
|
||||
|
||||
func mapMaintenanceEntry(entry *ent.MaintenanceEntry) MaintenanceEntry {
|
||||
return MaintenanceEntry{
|
||||
ID: entry.ID,
|
||||
Date: entry.Date,
|
||||
Name: entry.Name,
|
||||
Description: entry.Description,
|
||||
Cost: entry.Cost,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MaintenanceEntryRepository) Create(ctx context.Context, itemID uuid.UUID, input MaintenanceEntryCreate) (MaintenanceEntry, error) {
|
||||
item, err := r.db.MaintenanceEntry.Create().
|
||||
SetItemID(itemID).
|
||||
SetDate(input.Date).
|
||||
SetName(input.Name).
|
||||
SetDescription(input.Description).
|
||||
SetCost(input.Cost).
|
||||
Save(ctx)
|
||||
|
||||
return mapMaintenanceEntryErr(item, err)
|
||||
}
|
||||
|
||||
func (r *MaintenanceEntryRepository) Update(ctx context.Context, ID uuid.UUID, input MaintenanceEntryUpdate) (MaintenanceEntry, error) {
|
||||
item, err := r.db.MaintenanceEntry.UpdateOneID(ID).
|
||||
SetDate(input.Date).
|
||||
SetName(input.Name).
|
||||
SetDescription(input.Description).
|
||||
SetCost(input.Cost).
|
||||
Save(ctx)
|
||||
|
||||
return mapMaintenanceEntryErr(item, err)
|
||||
}
|
||||
|
||||
func (r *MaintenanceEntryRepository) GetLog(ctx context.Context, itemID uuid.UUID) (MaintenanceLog, error) {
|
||||
log := MaintenanceLog{
|
||||
ItemID: itemID,
|
||||
}
|
||||
|
||||
entries, err := r.db.MaintenanceEntry.Query().
|
||||
Where(maintenanceentry.ItemID(itemID)).
|
||||
Order(ent.Desc(maintenanceentry.FieldDate)).
|
||||
All(ctx)
|
||||
|
||||
if err != nil {
|
||||
return MaintenanceLog{}, err
|
||||
}
|
||||
|
||||
log.Entries = mapEachMaintenanceEntry(entries)
|
||||
|
||||
var maybeTotal *float64
|
||||
var maybeAverage *float64
|
||||
|
||||
q := `
|
||||
SELECT
|
||||
SUM(cost_total) AS total_of_totals,
|
||||
AVG(cost_total) AS avg_of_averages
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
strftime('%m-%Y', date) AS my,
|
||||
SUM(cost) AS cost_total
|
||||
FROM
|
||||
maintenance_entries
|
||||
WHERE
|
||||
item_id = ?
|
||||
GROUP BY
|
||||
my
|
||||
)`
|
||||
|
||||
row := r.db.Sql().QueryRowContext(ctx, q, itemID)
|
||||
err = row.Scan(&maybeTotal, &maybeAverage)
|
||||
if err != nil {
|
||||
return MaintenanceLog{}, err
|
||||
}
|
||||
|
||||
log.CostAverage = orDefault(maybeAverage, 0)
|
||||
log.CostTotal = orDefault(maybeTotal, 0)
|
||||
return log, nil
|
||||
}
|
||||
|
||||
func (r *MaintenanceEntryRepository) Delete(ctx context.Context, ID uuid.UUID) error {
|
||||
return r.db.MaintenanceEntry.DeleteOneID(ID).Exec(ctx)
|
||||
}
|
65
backend/internal/data/repo/repo_maintenance_entry_test.go
Normal file
65
backend/internal/data/repo/repo_maintenance_entry_test.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMaintenanceEntryRepository_GetLog(t *testing.T) {
|
||||
item := useItems(t, 1)[0]
|
||||
|
||||
// Create 10 maintenance entries for the item
|
||||
created := make([]MaintenanceEntryCreate, 10)
|
||||
|
||||
lastMonth := time.Now().AddDate(0, -1, 0)
|
||||
thisMonth := time.Now()
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
dt := lastMonth
|
||||
if i%2 == 0 {
|
||||
dt = thisMonth
|
||||
}
|
||||
|
||||
created[i] = MaintenanceEntryCreate{
|
||||
Date: dt,
|
||||
Name: "Maintenance",
|
||||
Description: "Maintenance description",
|
||||
Cost: 10,
|
||||
}
|
||||
}
|
||||
|
||||
for _, entry := range created {
|
||||
_, err := tRepos.MaintEntry.Create(context.Background(), item.ID, entry)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create maintenance entry: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the log for the item
|
||||
log, err := tRepos.MaintEntry.GetLog(context.Background(), item.ID)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get maintenance log: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, item.ID, log.ItemID)
|
||||
assert.Equal(t, 10, len(log.Entries))
|
||||
|
||||
// Calculate the average cost
|
||||
var total float64
|
||||
|
||||
for _, entry := range log.Entries {
|
||||
total += entry.Cost
|
||||
}
|
||||
|
||||
assert.Equal(t, total, log.CostTotal, "total cost should be equal to the sum of all entries")
|
||||
assert.Equal(t, total/2, log.CostAverage, "average cost should be the average of the two months")
|
||||
|
||||
for _, entry := range log.Entries {
|
||||
err := tRepos.MaintEntry.Delete(context.Background(), entry.ID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
|
@ -11,8 +11,8 @@ type AllRepos struct {
|
|||
Labels *LabelRepository
|
||||
Items *ItemsRepository
|
||||
Docs *DocumentRepository
|
||||
DocTokens *DocumentTokensRepository
|
||||
Attachments *AttachmentRepo
|
||||
MaintEntry *MaintenanceEntryRepository
|
||||
}
|
||||
|
||||
func New(db *ent.Client, root string) *AllRepos {
|
||||
|
@ -24,7 +24,7 @@ func New(db *ent.Client, root string) *AllRepos {
|
|||
Labels: &LabelRepository{db},
|
||||
Items: &ItemsRepository{db},
|
||||
Docs: &DocumentRepository{db, root},
|
||||
DocTokens: &DocumentTokensRepository{db},
|
||||
Attachments: &AttachmentRepo{db},
|
||||
MaintEntry: &MaintenanceEntryRepository{db},
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue