From 79566a7346e15e7a298c84eb4090f527f7c5a31d Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Fri, 24 Feb 2023 18:24:06 -0900 Subject: [PATCH] refactoring --- .../app/api/handlers/v1/v1_ctrl_reporting.go | 2 +- backend/internal/core/services/all.go | 11 +- .../services/reporting/bill_of_materials.go | 12 +- .../core/services/reporting/io_row.go | 1 + .../core/services/reporting/io_sheet.go | 8 +- .../core/services/reporting/reporting.go | 28 -- .../{io_row_test.go => value_parsers_test.go} | 0 .../internal/core/services/service_items.go | 244 +++++++++++++++++- .../data/repo/repo_maintenance_entry_test.go | 2 - backend/internal/data/repo/repo_users_test.go | 2 - backend/internal/data/types/date.go | 3 - 11 files changed, 252 insertions(+), 61 deletions(-) delete mode 100644 backend/internal/core/services/reporting/reporting.go rename backend/internal/core/services/reporting/{io_row_test.go => value_parsers_test.go} (100%) diff --git a/backend/app/api/handlers/v1/v1_ctrl_reporting.go b/backend/app/api/handlers/v1/v1_ctrl_reporting.go index f098c25..7792c1a 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_reporting.go +++ b/backend/app/api/handlers/v1/v1_ctrl_reporting.go @@ -19,7 +19,7 @@ func (ctrl *V1Controller) HandleBillOfMaterialsExport() server.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) error { actor := services.UseUserCtx(r.Context()) - csv, err := ctrl.svc.Reporting.BillOfMaterialsTSV(r.Context(), actor.GroupID) + csv, err := ctrl.svc.Items.ExportBillOfMaterialsTSV(r.Context(), actor.GroupID) if err != nil { return err } diff --git a/backend/internal/core/services/all.go b/backend/internal/core/services/all.go index 2997095..dab59ef 100644 --- a/backend/internal/core/services/all.go +++ b/backend/internal/core/services/all.go @@ -1,16 +1,13 @@ package services import ( - "github.com/hay-kot/homebox/backend/internal/core/services/reporting" "github.com/hay-kot/homebox/backend/internal/data/repo" - "github.com/rs/zerolog/log" ) type AllServices struct { - User *UserService - Group *GroupService - Items *ItemService - Reporting *reporting.ReportingService + User *UserService + Group *GroupService + Items *ItemService } type OptionsFunc func(*options) @@ -45,7 +42,5 @@ func New(repos *repo.AllRepos, opts ...OptionsFunc) *AllServices { repo: repos, autoIncrementAssetID: options.autoIncrementAssetID, }, - // TODO: don't use global logger - Reporting: reporting.NewReportingService(repos, &log.Logger), } } diff --git a/backend/internal/core/services/reporting/bill_of_materials.go b/backend/internal/core/services/reporting/bill_of_materials.go index 2cded8e..4147d4b 100644 --- a/backend/internal/core/services/reporting/bill_of_materials.go +++ b/backend/internal/core/services/reporting/bill_of_materials.go @@ -1,10 +1,8 @@ package reporting import ( - "context" - "github.com/gocarina/gocsv" - "github.com/google/uuid" + "github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/types" ) @@ -24,13 +22,7 @@ type BillOfMaterialsEntry struct { // BillOfMaterialsTSV returns a byte slice of the Bill of Materials for a given GID in TSV format // See BillOfMaterialsEntry for the format of the output -func (rs *ReportingService) BillOfMaterialsTSV(ctx context.Context, GID uuid.UUID) ([]byte, error) { - entities, err := rs.repos.Items.GetAll(ctx, GID) - if err != nil { - rs.l.Debug().Err(err).Msg("failed to get all items for BOM Csv Reporting") - return nil, err - } - +func BillOfMaterialsTSV(entities []repo.ItemOut) ([]byte, error) { bomEntries := make([]BillOfMaterialsEntry, len(entities)) for i, entity := range entities { bomEntries[i] = BillOfMaterialsEntry{ diff --git a/backend/internal/core/services/reporting/io_row.go b/backend/internal/core/services/reporting/io_row.go index dd1f58a..bccd877 100644 --- a/backend/internal/core/services/reporting/io_row.go +++ b/backend/internal/core/services/reporting/io_row.go @@ -17,6 +17,7 @@ type ExportTSVRow struct { LabelStr LabelString `csv:"HB.labels"` ImportRef string `csv:"HB.import_ref"` AssetID repo.AssetID `csv:"HB.asset_id"` + Archived bool `csv:"HB.archived"` Name string `csv:"HB.name"` Quantity int `csv:"HB.quantity"` diff --git a/backend/internal/core/services/reporting/io_sheet.go b/backend/internal/core/services/reporting/io_sheet.go index 4aa05df..c0f6eaf 100644 --- a/backend/internal/core/services/reporting/io_sheet.go +++ b/backend/internal/core/services/reporting/io_sheet.go @@ -10,6 +10,7 @@ import ( "github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/types" + "github.com/rs/zerolog/log" ) // IOSheet is the representation of a CSV/TSV sheet that is used for importing/exporting @@ -115,7 +116,11 @@ func (s *IOSheet) Read(data io.Reader) error { v = parseLabelString(val) } - fmt.Printf("%s: %v (%T)\n", tag, v, v) + log.Debug(). + Str("tag", tag). + Interface("val", v). + Str("type", fmt.Sprintf("%T", v)). + Msg("parsed value") // Nil values are not allowed at the moment. This may change. if v == nil { @@ -185,6 +190,7 @@ func (s *IOSheet) ReadItems(items []repo.ItemOut) { Quantity: item.Quantity, Description: item.Description, Insured: item.Insured, + Archived: item.Archived, PurchasePrice: item.PurchasePrice, PurchaseFrom: item.PurchaseFrom, diff --git a/backend/internal/core/services/reporting/reporting.go b/backend/internal/core/services/reporting/reporting.go deleted file mode 100644 index 8fa5efc..0000000 --- a/backend/internal/core/services/reporting/reporting.go +++ /dev/null @@ -1,28 +0,0 @@ -package reporting - -import ( - "encoding/csv" - "io" - - "github.com/gocarina/gocsv" - "github.com/hay-kot/homebox/backend/internal/data/repo" - "github.com/rs/zerolog" -) - -type ReportingService struct { - repos *repo.AllRepos - l *zerolog.Logger -} - -func NewReportingService(repos *repo.AllRepos, l *zerolog.Logger) *ReportingService { - gocsv.SetCSVWriter(func(out io.Writer) *gocsv.SafeCSVWriter { - writer := csv.NewWriter(out) - writer.Comma = '\t' - return gocsv.NewSafeCSVWriter(writer) - }) - - return &ReportingService{ - repos: repos, - l: l, - } -} diff --git a/backend/internal/core/services/reporting/io_row_test.go b/backend/internal/core/services/reporting/value_parsers_test.go similarity index 100% rename from backend/internal/core/services/reporting/io_row_test.go rename to backend/internal/core/services/reporting/value_parsers_test.go diff --git a/backend/internal/core/services/service_items.go b/backend/internal/core/services/service_items.go index 26b736d..9dd19ed 100644 --- a/backend/internal/core/services/service_items.go +++ b/backend/internal/core/services/service_items.go @@ -3,7 +3,9 @@ package services import ( "context" "errors" + "fmt" "io" + "strings" "github.com/google/uuid" "github.com/hay-kot/homebox/backend/internal/core/services/reporting" @@ -62,13 +64,234 @@ func (svc *ItemService) EnsureAssetID(ctx context.Context, GID uuid.UUID) (int, return finished, nil } -func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Reader) (int, error) { - // loaded, err := reporting.ReadCSV(data) - // if err != nil { - // return 0, err - // } +func serializeLocation[T ~[]string](location T) string { + return strings.Join(location, "/") +} - return 0, nil +// CsvImport imports items from a CSV file. using the standard defined format. +// +// CsvImport applies the following rules/operations +// +// 1. If the item does not exist, it is created. +// 2. If the item has a ImportRef and it exists it is skipped +// 3. Locations and Labels are created if they do not exist. +func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Reader) (int, error) { + sheet := reporting.IOSheet{} + + err := sheet.Read(data) + if err != nil { + return 0, err + } + + // ======================================== + // Labels + + labelMap := make(map[string]uuid.UUID) + { + labels, err := svc.repo.Labels.GetAll(ctx, GID) + if err != nil { + return 0, err + } + + for _, label := range labels { + labelMap[label.Name] = label.ID + } + } + + // ======================================== + // Locations + + locationMap := make(map[string]uuid.UUID) + { + locations, err := svc.repo.Locations.Tree(ctx, GID, repo.TreeQuery{WithItems: false}) + if err != nil { + return 0, err + } + + // Traverse the tree and build a map of location full paths to IDs + // where the full path is the location name joined by slashes. + var traverse func(location *repo.TreeItem, path []string) + traverse = func(location *repo.TreeItem, path []string) { + path = append(path, location.Name) + + locationMap[serializeLocation(path)] = location.ID + + for _, child := range location.Children { + traverse(child, path) + } + } + + for _, location := range locations { + traverse(&location, []string{}) + } + } + + // ======================================== + // Import items + + // Asset ID Pre-Check + highestAID := repo.AssetID(-1) + if svc.autoIncrementAssetID { + highestAID, err = svc.repo.Items.GetHighestAssetID(ctx, GID) + if err != nil { + return 0, err + } + } + + finished := 0 + + for i := range sheet.Rows { + row := sheet.Rows[i] + + // ======================================== + // Preflight check for existing item + // TODO: Allow updates to existing items by matching on ImportRef + if row.ImportRef != "" { + exists, err := svc.repo.Items.CheckRef(ctx, GID, row.ImportRef) + if err != nil { + return 0, fmt.Errorf("error checking for existing item with ref %q: %w", row.ImportRef, err) + } + + if exists { + continue + } + } + + // ======================================== + // Pre-Create Labels as necessary + labelIds := make([]uuid.UUID, len(row.LabelStr)) + + for j := range row.LabelStr { + label := row.LabelStr[j] + + id, ok := labelMap[label] + if !ok { + newLabel, err := svc.repo.Labels.Create(ctx, GID, repo.LabelCreate{Name: label}) + if err != nil { + return 0, err + } + id = newLabel.ID + } + + labelIds[j] = id + labelMap[label] = id + } + + // ======================================== + // Pre-Create Locations as necessary + path := serializeLocation(row.Location) + + locationID, ok := locationMap[path] + if !ok { // Traverse the path of LocationStr and check each path element to see if it exists already, if not create it. + paths := []string{} + for i, pathElement := range row.Location { + paths = append(paths, pathElement) + path := serializeLocation(paths) + + locationID, ok = locationMap[path] + if !ok { + parentID := uuid.Nil + + // Get the parent ID + if i > 0 { + parentPath := serializeLocation(row.Location[:i]) + parentID = locationMap[parentPath] + } + + newLocation, err := svc.repo.Locations.Create(ctx, GID, repo.LocationCreate{ + ParentID: parentID, + Name: pathElement, + }) + if err != nil { + return 0, err + } + locationID = newLocation.ID + } + + locationMap[path] = locationID + } + + locationID, ok = locationMap[path] + if !ok { + return 0, errors.New("failed to create location") + } + } + + var effAID repo.AssetID + if svc.autoIncrementAssetID && row.AssetID.Nil() { + effAID = highestAID + 1 + highestAID++ + } else { + effAID = row.AssetID + } + + // ======================================== + // Create Item + newItem := repo.ItemCreate{ + ImportRef: row.ImportRef, + Name: row.Name, + Description: row.Description, + AssetID: effAID, + LocationID: locationID, + LabelIDs: labelIds, + } + + item, err := svc.repo.Items.Create(ctx, GID, newItem) + if err != nil { + return 0, err + } + + fields := make([]repo.ItemField, len(row.Fields)) + for i := range row.Fields { + fields[i] = repo.ItemField{ + Name: row.Fields[i].Name, + Type: "text", + TextValue: row.Fields[i].Value, + } + } + + updateItem := repo.ItemUpdate{ + ID: item.ID, + LabelIDs: labelIds, + LocationID: locationID, + + Name: row.Name, + Description: row.Description, + AssetID: effAID, + Insured: row.Insured, + Quantity: row.Quantity, + Archived: row.Archived, + + PurchasePrice: row.PurchasePrice, + PurchaseFrom: row.PurchaseFrom, + PurchaseTime: row.PurchaseTime, + + Manufacturer: row.Manufacturer, + ModelNumber: row.ModelNumber, + SerialNumber: row.SerialNumber, + + LifetimeWarranty: row.LifetimeWarranty, + WarrantyExpires: row.WarrantyExpires, + WarrantyDetails: row.WarrantyDetails, + + SoldTo: row.SoldTo, + SoldTime: row.SoldTime, + SoldPrice: row.SoldPrice, + SoldNotes: row.SoldNotes, + + Notes: row.Notes, + Fields: fields, + } + + item, err = svc.repo.Items.UpdateByGroup(ctx, GID, updateItem) + if err != nil { + return 0, err + } + + finished++ + } + + return finished, nil } func (svc *ItemService) ExportTSV(ctx context.Context, GID uuid.UUID) ([][]string, error) { @@ -83,3 +306,12 @@ func (svc *ItemService) ExportTSV(ctx context.Context, GID uuid.UUID) ([][]strin return sheet.TSV() } + +func (svc *ItemService) ExportBillOfMaterialsTSV(ctx context.Context, GID uuid.UUID) ([]byte, error) { + items, err := svc.repo.Items.GetAll(ctx, GID) + if err != nil { + return nil, err + } + + return reporting.BillOfMaterialsTSV(items) +} diff --git a/backend/internal/data/repo/repo_maintenance_entry_test.go b/backend/internal/data/repo/repo_maintenance_entry_test.go index bc9f6af..aafb08e 100644 --- a/backend/internal/data/repo/repo_maintenance_entry_test.go +++ b/backend/internal/data/repo/repo_maintenance_entry_test.go @@ -16,9 +16,7 @@ func getPrevMonth(now time.Time) time.Time { // avoid infinite loop max := 15 for t.Month() == now.Month() { - println("month is the same") t = t.AddDate(0, 0, -1) - println(t.String()) max-- if max == 0 { diff --git a/backend/internal/data/repo/repo_users_test.go b/backend/internal/data/repo/repo_users_test.go index 31d2737..d3cd361 100644 --- a/backend/internal/data/repo/repo_users_test.go +++ b/backend/internal/data/repo/repo_users_test.go @@ -2,7 +2,6 @@ package repo import ( "context" - "fmt" "testing" "github.com/stretchr/testify/assert" @@ -81,7 +80,6 @@ func TestUserRepo_GetAll(t *testing.T) { 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) diff --git a/backend/internal/data/types/date.go b/backend/internal/data/types/date.go index 0dc09db..1b8b182 100644 --- a/backend/internal/data/types/date.go +++ b/backend/internal/data/types/date.go @@ -2,7 +2,6 @@ package types import ( "errors" - "fmt" "strings" "time" ) @@ -74,9 +73,7 @@ func (d Date) MarshalJSON() ([]byte, error) { func (d *Date) UnmarshalJSON(data []byte) (err error) { // unescape the string if necessary `\"` -> `"` str := strings.Trim(string(data), "\"") - fmt.Printf("str: %q\n", str) if str == "" || str == "null" || str == `""` { - println("empty date") *d = Date{} return nil }