feat: allow nested relationships for locations and items (#102)

Basic implementation that allows organizing Locations and Items within each other.
This commit is contained in:
Hayden 2022-10-23 20:54:39 -08:00 committed by GitHub
parent fe6cd431a6
commit a4b4fe3454
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 2329 additions and 126 deletions

View file

@ -0,0 +1,28 @@
-- disable the enforcement of foreign-keys constraints
PRAGMA foreign_keys = off;
-- create "new_items" table
CREATE TABLE `new_items` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `name` text NOT NULL, `description` text NULL, `import_ref` text NULL, `notes` text NULL, `quantity` integer NOT NULL DEFAULT 1, `insured` bool NOT NULL DEFAULT false, `serial_number` text NULL, `model_number` text NULL, `manufacturer` text NULL, `lifetime_warranty` bool NOT NULL DEFAULT false, `warranty_expires` datetime NULL, `warranty_details` text NULL, `purchase_time` datetime NULL, `purchase_from` text NULL, `purchase_price` real NOT NULL DEFAULT 0, `sold_time` datetime NULL, `sold_to` text NULL, `sold_price` real NOT NULL DEFAULT 0, `sold_notes` text NULL, `group_items` uuid NOT NULL, `item_children` uuid NULL, `location_items` uuid NULL, PRIMARY KEY (`id`), CONSTRAINT `items_groups_items` FOREIGN KEY (`group_items`) REFERENCES `groups` (`id`) ON DELETE CASCADE, CONSTRAINT `items_items_children` FOREIGN KEY (`item_children`) REFERENCES `items` (`id`) ON DELETE SET NULL, CONSTRAINT `items_locations_items` FOREIGN KEY (`location_items`) REFERENCES `locations` (`id`) ON DELETE CASCADE);
-- copy rows from old table "items" to new temporary table "new_items"
INSERT INTO `new_items` (`id`, `created_at`, `updated_at`, `name`, `description`, `import_ref`, `notes`, `quantity`, `insured`, `serial_number`, `model_number`, `manufacturer`, `lifetime_warranty`, `warranty_expires`, `warranty_details`, `purchase_time`, `purchase_from`, `purchase_price`, `sold_time`, `sold_to`, `sold_price`, `sold_notes`, `group_items`, `location_items`) SELECT `id`, `created_at`, `updated_at`, `name`, `description`, `import_ref`, `notes`, `quantity`, `insured`, `serial_number`, `model_number`, `manufacturer`, `lifetime_warranty`, `warranty_expires`, `warranty_details`, `purchase_time`, `purchase_from`, `purchase_price`, `sold_time`, `sold_to`, `sold_price`, `sold_notes`, `group_items`, `location_items` FROM `items`;
-- drop "items" table after copying rows
DROP TABLE `items`;
-- rename temporary table "new_items" to "items"
ALTER TABLE `new_items` RENAME TO `items`;
-- create index "item_name" to table: "items"
CREATE INDEX `item_name` ON `items` (`name`);
-- create index "item_manufacturer" to table: "items"
CREATE INDEX `item_manufacturer` ON `items` (`manufacturer`);
-- create index "item_model_number" to table: "items"
CREATE INDEX `item_model_number` ON `items` (`model_number`);
-- create index "item_serial_number" to table: "items"
CREATE INDEX `item_serial_number` ON `items` (`serial_number`);
-- create "new_locations" table
CREATE TABLE `new_locations` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `name` text NOT NULL, `description` text NULL, `group_locations` uuid NOT NULL, `location_children` uuid NULL, PRIMARY KEY (`id`), CONSTRAINT `locations_groups_locations` FOREIGN KEY (`group_locations`) REFERENCES `groups` (`id`) ON DELETE CASCADE, CONSTRAINT `locations_locations_children` FOREIGN KEY (`location_children`) REFERENCES `locations` (`id`) ON DELETE SET NULL);
-- copy rows from old table "locations" to new temporary table "new_locations"
INSERT INTO `new_locations` (`id`, `created_at`, `updated_at`, `name`, `description`, `group_locations`) SELECT `id`, `created_at`, `updated_at`, `name`, `description`, `group_locations` FROM `locations`;
-- drop "locations" table after copying rows
DROP TABLE `locations`;
-- rename temporary table "new_locations" to "locations"
ALTER TABLE `new_locations` RENAME TO `locations`;
-- enable back the enforcement of foreign-keys constraints
PRAGMA foreign_keys = on;

View file

@ -1,4 +1,5 @@
h1:nN8ZUHScToap+2yJKsb+AnKu8o2DubUoiKc9neQJ93M=
h1:mYTnmyrnBDST/r93NGJM33mIJqhp/U9qR440zI99eqQ=
20220929052825_init.sql h1:ZlCqm1wzjDmofeAcSX3jE4h4VcdTNGpRg2eabztDy9Q=
20221001210956_group_invitations.sql h1:YQKJFtE39wFOcRNbZQ/d+ZlHwrcfcsZlcv/pLEYdpjw=
20221009173029_add_user_roles.sql h1:vWmzAfgEWQeGk0Vn70zfVPCcfEZth3E0JcvyKTjpYyU=
20221020043305_allow_nesting_types.sql h1:4AyJpZ7l7SSJtJAQETYY802FHJ64ufYPJTqvwdiGn3M=

View file

@ -39,15 +39,17 @@ type (
}
ItemCreate struct {
ImportRef string `json:"-"`
Name string `json:"name"`
Description string `json:"description"`
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"`
@ -95,11 +97,12 @@ type (
UpdatedAt time.Time `json:"updatedAt"`
// Edges
Location LocationSummary `json:"location"`
Labels []LabelSummary `json:"labels"`
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"`
@ -126,8 +129,8 @@ type (
Notes string `json:"notes"`
Attachments []ItemAttachment `json:"attachments"`
// Future
Fields []ItemField `json:"fields"`
Fields []ItemField `json:"fields"`
Children []ItemSummary `json:"children"`
}
)
@ -136,12 +139,13 @@ var (
)
func mapItemSummary(item *ent.Item) ItemSummary {
var location LocationSummary
var location *LocationSummary
if item.Edges.Location != nil {
location = mapLocationSummary(item.Edges.Location)
loc := mapLocationSummary(item.Edges.Location)
location = &loc
}
var labels []LabelSummary
labels := make([]LabelSummary, len(item.Edges.Label))
if item.Edges.Label != nil {
labels = mapEach(item.Edges.Label, mapLabelSummary)
}
@ -194,7 +198,19 @@ func mapItemOut(item *ent.Item) ItemOut {
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,
@ -220,6 +236,7 @@ func mapItemOut(item *ent.Item) ItemOut {
Notes: item.Notes,
Attachments: attachments,
Fields: fields,
Children: children,
}
}
@ -231,6 +248,8 @@ func (e *ItemsRepository) getOne(ctx context.Context, where ...predicate.Item) (
WithLabel().
WithLocation().
WithGroup().
WithChildren().
WithParent().
WithAttachments(func(aq *ent.AttachmentQuery) {
aq.WithDocument()
}).
@ -398,6 +417,12 @@ func (e *ItemsRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data
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

View file

@ -41,6 +41,42 @@ func useItems(t *testing.T, len int) []ItemOut {
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)

View file

@ -22,6 +22,7 @@ type (
}
LocationUpdate struct {
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable"`
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
@ -41,8 +42,10 @@ type (
}
LocationOut struct {
Parent *LocationSummary `json:"parent,omitempty"`
LocationSummary
Items []ItemSummary `json:"items"`
Items []ItemSummary `json:"items"`
Children []LocationSummary `json:"children"`
}
)
@ -61,7 +64,20 @@ var (
)
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,
@ -125,6 +141,8 @@ func (r *LocationRepository) getOne(ctx context.Context, where ...predicate.Loca
WithItems(func(iq *ent.ItemQuery) {
iq.WithLabel()
}).
WithParent().
WithChildren().
Only(ctx))
}
@ -152,10 +170,17 @@ func (r *LocationRepository) Create(ctx context.Context, gid uuid.UUID, data Loc
}
func (r *LocationRepository) Update(ctx context.Context, data LocationUpdate) (LocationOut, error) {
_, err := r.db.Location.UpdateOneID(data.ID).
q := r.db.Location.UpdateOneID(data.ID).
SetName(data.Name).
SetDescription(data.Description).
Save(ctx)
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