fix import bug and add ref support (#88)

* fix import bug and add ref support

* fix calls

* add docs
This commit is contained in:
Hayden 2022-10-15 17:46:57 -08:00 committed by GitHub
parent 5596740cd2
commit dbaaf4ad0a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 118 additions and 57 deletions

View file

@ -52,7 +52,7 @@ func (a *app) SetupDemo() {
log.Fatal().Msg("Failed to setup demo") log.Fatal().Msg("Failed to setup demo")
} }
err = a.services.Items.CsvImport(context.Background(), self.GroupID, records) _, err = a.services.Items.CsvImport(context.Background(), self.GroupID, records)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to import CSV") log.Err(err).Msg("Failed to import CSV")
log.Fatal().Msg("Failed to setup demo") log.Fatal().Msg("Failed to setup demo")

View file

@ -220,7 +220,7 @@ func (ctrl *V1Controller) HandleItemsImport() http.HandlerFunc {
user := services.UseUserCtx(r.Context()) user := services.UseUserCtx(r.Context())
err = ctrl.svc.Items.CsvImport(r.Context(), user.GroupID, data) _, err = ctrl.svc.Items.CsvImport(r.Context(), user.GroupID, data)
if err != nil { if err != nil {
log.Err(err).Msg("failed to import items") log.Err(err).Msg("failed to import items")
server.RespondServerError(w) server.RespondServerError(w)

View file

@ -211,6 +211,11 @@ func (e *ItemsRepository) GetOne(ctx context.Context, id uuid.UUID) (ItemOut, er
return e.getOne(ctx, item.ID(id)) 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 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. // GetOneByGroup ensures that the item belongs to a specific group.
func (e *ItemsRepository) GetOneByGroup(ctx context.Context, gid, id uuid.UUID) (ItemOut, error) { func (e *ItemsRepository) GetOneByGroup(ctx context.Context, gid, id uuid.UUID) (ItemOut, error) {
@ -287,6 +292,7 @@ func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]ItemSumm
func (e *ItemsRepository) Create(ctx context.Context, gid uuid.UUID, data ItemCreate) (ItemOut, error) { func (e *ItemsRepository) Create(ctx context.Context, gid uuid.UUID, data ItemCreate) (ItemOut, error) {
q := e.db.Item.Create(). q := e.db.Item.Create().
SetImportRef(data.ImportRef).
SetName(data.Name). SetName(data.Name).
SetDescription(data.Description). SetDescription(data.Description).
SetGroupID(gid). SetGroupID(gid).

View file

@ -3,7 +3,6 @@ package services
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/repo" "github.com/hay-kot/homebox/backend/internal/repo"
@ -48,7 +47,7 @@ func (svc *ItemService) Update(ctx context.Context, gid uuid.UUID, data repo.Ite
return svc.repo.Items.UpdateByGroup(ctx, gid, data) return svc.repo.Items.UpdateByGroup(ctx, gid, data)
} }
func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]string) error { func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]string) (int, error) {
loaded := []csvRow{} loaded := []csvRow{}
// Skip first row // Skip first row
@ -59,18 +58,41 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
} }
if len(row) != NumOfCols { if len(row) != NumOfCols {
return ErrInvalidCsv return 0, ErrInvalidCsv
} }
r := newCsvRow(row) r := newCsvRow(row)
loaded = append(loaded, r) loaded = append(loaded, r)
} }
// validate rows
var errMap = map[int][]error{}
var hasErr bool
for i, r := range loaded {
errs := r.validate()
if len(errs) > 0 {
hasErr = true
lineNum := i + 2
errMap[lineNum] = errs
}
}
if hasErr {
for lineNum, errs := range errMap {
for _, err := range errs {
log.Error().Err(err).Int("line", lineNum).Msg("csv import error")
}
}
}
// Bootstrap the locations and labels so we can reuse the created IDs for the items // Bootstrap the locations and labels so we can reuse the created IDs for the items
locations := map[string]uuid.UUID{} locations := map[string]uuid.UUID{}
existingLocation, err := svc.repo.Locations.GetAll(ctx, gid) existingLocation, err := svc.repo.Locations.GetAll(ctx, gid)
if err != nil { if err != nil {
return err return 0, err
} }
for _, loc := range existingLocation { for _, loc := range existingLocation {
locations[loc.Name] = loc.ID locations[loc.Name] = loc.ID
@ -79,7 +101,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
labels := map[string]uuid.UUID{} labels := map[string]uuid.UUID{}
existingLabels, err := svc.repo.Labels.GetAll(ctx, gid) existingLabels, err := svc.repo.Labels.GetAll(ctx, gid)
if err != nil { if err != nil {
return err return 0, err
} }
for _, label := range existingLabels { for _, label := range existingLabels {
labels[label.Name] = label.ID labels[label.Name] = label.ID
@ -88,25 +110,21 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
for _, row := range loaded { for _, row := range loaded {
// Locations // Locations
if _, ok := locations[row.Location]; ok { if _, exists := locations[row.Location]; !exists {
continue
}
fmt.Println("Creating Location: ", row.Location)
result, err := svc.repo.Locations.Create(ctx, gid, repo.LocationCreate{ result, err := svc.repo.Locations.Create(ctx, gid, repo.LocationCreate{
Name: row.Location, Name: row.Location,
Description: "", Description: "",
}) })
if err != nil { if err != nil {
return err return 0, err
} }
locations[row.Location] = result.ID locations[row.Location] = result.ID
}
// Labels // Labels
for _, label := range row.getLabels() { for _, label := range row.getLabels() {
if _, ok := labels[label]; ok { if _, exists := labels[label]; exists {
continue continue
} }
result, err := svc.repo.Labels.Create(ctx, gid, repo.LabelCreate{ result, err := svc.repo.Labels.Create(ctx, gid, repo.LabelCreate{
@ -114,14 +132,26 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
Description: "", Description: "",
}) })
if err != nil { if err != nil {
return err return 0, err
} }
labels[label] = result.ID labels[label] = result.ID
} }
} }
// Create the items // Create the items
var count int
for _, row := range loaded { for _, row := range loaded {
// Check Import Ref
if row.Item.ImportRef != "" {
exists, err := svc.repo.Items.CheckRef(ctx, gid, row.Item.ImportRef)
if exists {
continue
}
if err != nil {
log.Err(err).Msg("error checking import ref")
}
}
locationID := locations[row.Location] locationID := locations[row.Location]
labelIDs := []uuid.UUID{} labelIDs := []uuid.UUID{}
for _, label := range row.getLabels() { for _, label := range row.getLabels() {
@ -131,8 +161,6 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
log.Info(). log.Info().
Str("name", row.Item.Name). Str("name", row.Item.Name).
Str("location", row.Location). Str("location", row.Location).
Strs("labels", row.getLabels()).
Str("locationId", locationID.String()).
Msgf("Creating Item: %s", row.Item.Name) Msgf("Creating Item: %s", row.Item.Name)
result, err := svc.repo.Items.Create(ctx, gid, repo.ItemCreate{ result, err := svc.repo.Items.Create(ctx, gid, repo.ItemCreate{
@ -144,7 +172,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
}) })
if err != nil { if err != nil {
return err return count, err
} }
// Update the item with the rest of the data // Update the item with the rest of the data
@ -183,8 +211,10 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
}) })
if err != nil { if err != nil {
return err return count, err
} }
count++
} }
return nil return count, nil
} }

View file

@ -97,3 +97,22 @@ func (c csvRow) getLabels() []string {
return split return split
} }
func (c csvRow) validate() []error {
var errs []error
add := func(err error) {
errs = append(errs, err)
}
required := func(s string, name string) {
if s == "" {
add(errors.New(name + " is required"))
}
}
required(c.Location, "Location")
required(c.Item.Name, "Name")
return errs
}

View file

@ -9,12 +9,12 @@ import (
const CSV_DATA = ` const CSV_DATA = `
Import Ref,Location,Labels,Quantity,Name,Description,Insured,Serial Number,Mode Number,Manufacturer,Notes,Purchase From,Purchased Price,Purchased Time,Lifetime Warranty,Warranty Expires,Warranty Details,Sold To,Sold Price,Sold Time,Sold Notes Import Ref,Location,Labels,Quantity,Name,Description,Insured,Serial Number,Mode Number,Manufacturer,Notes,Purchase From,Purchased Price,Purchased Time,Lifetime Warranty,Warranty Expires,Warranty Details,Sold To,Sold Price,Sold Time,Sold Notes
,Garage,IOT;Home Assistant; Z-Wave,1,Zooz Universal Relay ZEN17,"Zooz 700 Series Z-Wave Universal Relay ZEN17 for Awnings, Garage Doors, Sprinklers, and More | 2 NO-C-NC Relays (20A, 10A) | Signal Repeater | Hub Required (Compatible with SmartThings and Hubitat)",,,ZEN17,Zooz,,Amazon,39.95,10/13/2021,,,,,,, A,Garage,IOT;Home Assistant; Z-Wave,1,Zooz Universal Relay ZEN17,"Zooz 700 Series Z-Wave Universal Relay ZEN17 for Awnings, Garage Doors, Sprinklers, and More | 2 NO-C-NC Relays (20A, 10A) | Signal Repeater | Hub Required (Compatible with SmartThings and Hubitat)",,,ZEN17,Zooz,,Amazon,39.95,10/13/2021,,,,,,,
,Living Room,IOT;Home Assistant; Z-Wave,1,Zooz Motion Sensor,"Zooz Z-Wave Plus S2 Motion Sensor ZSE18 with Magnetic Mount, Works with Vera and SmartThings",,,ZSE18,Zooz,,Amazon,29.95,10/15/2021,,,,,,, B,Living Room,IOT;Home Assistant; Z-Wave,1,Zooz Motion Sensor,"Zooz Z-Wave Plus S2 Motion Sensor ZSE18 with Magnetic Mount, Works with Vera and SmartThings",,,ZSE18,Zooz,,Amazon,29.95,10/15/2021,,,,,,,
,Office,IOT;Home Assistant; Z-Wave,1,Zooz 110v Power Switch,"Zooz Z-Wave Plus Power Switch ZEN15 for 110V AC Units, Sump Pumps, Humidifiers, and More",,,ZEN15,Zooz,,Amazon,39.95,10/13/2021,,,,,,, C,Office,IOT;Home Assistant; Z-Wave,1,Zooz 110v Power Switch,"Zooz Z-Wave Plus Power Switch ZEN15 for 110V AC Units, Sump Pumps, Humidifiers, and More",,,ZEN15,Zooz,,Amazon,39.95,10/13/2021,,,,,,,
,Downstairs,IOT;Home Assistant; Z-Wave,1,Ecolink Z-Wave PIR Motion Sensor,"Ecolink Z-Wave PIR Motion Detector Pet Immune, White (PIRZWAVE2.5-ECO)",,,PIRZWAVE2.5-ECO,Ecolink,,Amazon,35.58,10/21/2020,,,,,,, D,Downstairs,IOT;Home Assistant; Z-Wave,1,Ecolink Z-Wave PIR Motion Sensor,"Ecolink Z-Wave PIR Motion Detector Pet Immune, White (PIRZWAVE2.5-ECO)",,,PIRZWAVE2.5-ECO,Ecolink,,Amazon,35.58,10/21/2020,,,,,,,
,Entry,IOT;Home Assistant; Z-Wave,1,Yale Security Touchscreen Deadbolt,"Yale Security YRD226-ZW2-619 YRD226ZW2619 Touchscreen Deadbolt, Satin Nickel",,,YRD226ZW2619,Yale,,Amazon,120.39,10/14/2020,,,,,,, E,Entry,IOT;Home Assistant; Z-Wave,1,Yale Security Touchscreen Deadbolt,"Yale Security YRD226-ZW2-619 YRD226ZW2619 Touchscreen Deadbolt, Satin Nickel",,,YRD226ZW2619,Yale,,Amazon,120.39,10/14/2020,,,,,,,
,Kitchen,IOT;Home Assistant; Z-Wave,1,Smart Rocker Light Dimmer,"UltraPro Z-Wave Smart Rocker Light Dimmer with QuickFit and SimpleWire, 3-Way Ready, Compatible with Alexa, Google Assistant, ZWave Hub Required, Repeater/Range Extender, White Paddle Only, 39351",,,39351,Honeywell,,Amazon,65.98,09/30/0202,,,,,,,` F,Kitchen,IOT;Home Assistant; Z-Wave,1,Smart Rocker Light Dimmer,"UltraPro Z-Wave Smart Rocker Light Dimmer with QuickFit and SimpleWire, 3-Way Ready, Compatible with Alexa, Google Assistant, ZWave Hub Required, Repeater/Range Extender, White Paddle Only, 39351",,,39351,Honeywell,,Amazon,65.98,09/30/0202,,,,,,,`
func loadcsv() [][]string { func loadcsv() [][]string {
reader := csv.NewReader(bytes.NewBuffer([]byte(CSV_DATA))) reader := csv.NewReader(bytes.NewBuffer([]byte(CSV_DATA)))

View file

@ -13,7 +13,13 @@ func TestItemService_CsvImport(t *testing.T) {
svc := &ItemService{ svc := &ItemService{
repo: tRepos, repo: tRepos,
} }
err := svc.CsvImport(context.Background(), tGroup.ID, data) count, err := svc.CsvImport(context.Background(), tGroup.ID, data)
assert.Equal(t, 6, count)
assert.NoError(t, err)
// Check import refs are deduplicated
count, err = svc.CsvImport(context.Background(), tGroup.ID, data)
assert.Equal(t, 0, count)
assert.NoError(t, err) assert.NoError(t, err)
items, err := svc.GetAll(context.Background(), tGroup.ID) items, err := svc.GetAll(context.Background(), tGroup.ID)

View file

@ -24,8 +24,8 @@ Import RefLocation Labels Quantity Name Description Insured Serial Number Model
## CSV Reference ## CSV Reference
| Column | Type | Description | | Column | Type | Description |
| ----------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | ----------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ImportRef | String (100) | Future | | ImportRef | String (100) | Import Refs are unique strings that can be used to deduplicate imports. Before an item is imported, we check the database for a matching ref. If the ref exists, we skip that item. |
| Location | String | This is the location of the item that will be created. These are de-duplicated and won't create another instance when reused. | | Location | String | This is the location of the item that will be created. These are de-duplicated and won't create another instance when reused. |
| Labels | `;` Separated String | List of labels to apply to the item separated by a `;`, can be existing or new | | Labels | `;` Separated String | List of labels to apply to the item separated by a `;`, can be existing or new |
| Quantity | Integer | The quantity of items to create | | Quantity | Integer | The quantity of items to create |