homebox/backend/internal/data/repo/repo_locations.go

352 lines
8.6 KiB
Go
Raw Normal View History

2022-08-31 03:21:18 +00:00
package repo
import (
"context"
"strings"
"time"
2022-08-31 03:21:18 +00:00
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/ent/group"
"github.com/hay-kot/homebox/backend/internal/data/ent/item"
"github.com/hay-kot/homebox/backend/internal/data/ent/location"
"github.com/hay-kot/homebox/backend/internal/data/ent/predicate"
2022-08-31 03:21:18 +00:00
)
type LocationRepository struct {
2022-08-31 03:21:18 +00:00
db *ent.Client
}
type (
LocationCreate struct {
Name string `json:"name"`
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable"`
Description string `json:"description"`
}
LocationUpdate struct {
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable"`
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
LocationSummary struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
LocationOutCount struct {
LocationSummary
ItemCount int `json:"itemCount"`
}
LocationOut struct {
Parent *LocationSummary `json:"parent,omitempty"`
LocationSummary
Items []ItemSummary `json:"items"`
Children []LocationSummary `json:"children"`
}
)
func mapLocationSummary(location *ent.Location) LocationSummary {
return LocationSummary{
ID: location.ID,
Name: location.Name,
Description: location.Description,
CreatedAt: location.CreatedAt,
UpdatedAt: location.UpdatedAt,
}
}
var mapLocationOutErr = mapTErrFunc(mapLocationOut)
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,
Description: location.Description,
CreatedAt: location.CreatedAt,
UpdatedAt: location.UpdatedAt,
},
Items: mapEach(location.Edges.Items, mapItemSummary),
}
2022-09-03 09:17:48 +00:00
}
type LocationQuery struct {
FilterChildren bool `json:"filterChildren"`
}
2022-09-03 09:17:48 +00:00
// GetALlWithCount returns all locations with item count field populated
func (r *LocationRepository) GetAll(ctx context.Context, GID uuid.UUID, filter LocationQuery) ([]LocationOutCount, error) {
query := `--sql
2022-09-03 09:17:48 +00:00
SELECT
id,
name,
description,
created_at,
updated_at,
(
SELECT
COUNT(*)
FROM
items
WHERE
items.location_items = locations.id
AND items.archived = false
2022-09-03 09:17:48 +00:00
) as item_count
FROM
locations
WHERE
locations.group_locations = ? {{ FILTER_CHILDREN }}
ORDER BY
locations.name ASC
`
2022-09-03 09:17:48 +00:00
if filter.FilterChildren {
query = strings.Replace(query, "{{ FILTER_CHILDREN }}", "AND locations.location_children IS NULL", 1)
} else {
query = strings.Replace(query, "{{ FILTER_CHILDREN }}", "", 1)
}
rows, err := r.db.Sql().QueryContext(ctx, query, GID)
2022-09-03 09:17:48 +00:00
if err != nil {
return nil, err
}
list := []LocationOutCount{}
2022-09-03 09:17:48 +00:00
for rows.Next() {
var ct LocationOutCount
err := rows.Scan(&ct.ID, &ct.Name, &ct.Description, &ct.CreatedAt, &ct.UpdatedAt, &ct.ItemCount)
2022-09-03 09:17:48 +00:00
if err != nil {
return nil, err
}
2022-09-03 09:17:48 +00:00
list = append(list, ct)
}
return list, err
}
func (r *LocationRepository) getOne(ctx context.Context, where ...predicate.Location) (LocationOut, error) {
return mapLocationOutErr(r.db.Location.Query().
Where(where...).
2022-09-01 22:32:03 +00:00
WithGroup().
2022-09-03 09:17:48 +00:00
WithItems(func(iq *ent.ItemQuery) {
iq.Where(item.Archived(false)).
Order(ent.Asc(item.FieldName)).
WithLabel()
2022-09-03 09:17:48 +00:00
}).
WithParent().
WithChildren().
Only(ctx))
2022-08-31 03:21:18 +00:00
}
func (r *LocationRepository) Get(ctx context.Context, ID uuid.UUID) (LocationOut, error) {
return r.getOne(ctx, location.ID(ID))
}
func (r *LocationRepository) GetOneByGroup(ctx context.Context, GID, ID uuid.UUID) (LocationOut, error) {
return r.getOne(ctx, location.ID(ID), location.HasGroupWith(group.ID(GID)))
}
func (r *LocationRepository) Create(ctx context.Context, GID uuid.UUID, data LocationCreate) (LocationOut, error) {
q := r.db.Location.Create().
2022-08-31 03:21:18 +00:00
SetName(data.Name).
SetDescription(data.Description).
SetGroupID(GID)
if data.ParentID != uuid.Nil {
q.SetParentID(data.ParentID)
}
location, err := q.Save(ctx)
2022-09-06 19:15:07 +00:00
if err != nil {
return LocationOut{}, err
2022-09-06 19:15:07 +00:00
}
location.Edges.Group = &ent.Group{ID: GID} // bootstrap group ID
return mapLocationOut(location), nil
2022-08-31 03:21:18 +00:00
}
func (r *LocationRepository) update(ctx context.Context, data LocationUpdate, where ...predicate.Location) (LocationOut, error) {
q := r.db.Location.Update().
Where(where...).
2022-08-31 03:21:18 +00:00
SetName(data.Name).
SetDescription(data.Description)
if data.ParentID != uuid.Nil {
q.SetParentID(data.ParentID)
} else {
q.ClearParent()
}
_, err := q.Save(ctx)
2022-09-01 22:32:03 +00:00
if err != nil {
return LocationOut{}, err
2022-09-01 22:32:03 +00:00
}
return r.Get(ctx, data.ID)
2022-08-31 03:21:18 +00:00
}
func (r *LocationRepository) Update(ctx context.Context, data LocationUpdate) (LocationOut, error) {
return r.update(ctx, data, location.ID(data.ID))
}
func (r *LocationRepository) UpdateOneByGroup(ctx context.Context, GID, ID uuid.UUID, data LocationUpdate) (LocationOut, error) {
return r.update(ctx, data, location.ID(ID), location.HasGroupWith(group.ID(GID)))
}
func (r *LocationRepository) Delete(ctx context.Context, ID uuid.UUID) error {
return r.db.Location.DeleteOneID(ID).Exec(ctx)
}
func (r *LocationRepository) DeleteByGroup(ctx context.Context, GID, ID uuid.UUID) error {
_, err := r.db.Location.Delete().Where(location.ID(ID), location.HasGroupWith(group.ID(GID))).Exec(ctx)
return err
2022-08-31 03:21:18 +00:00
}
type TreeItem struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Children []*TreeItem `json:"children"`
}
type FlatTreeItem struct {
ID uuid.UUID
Name string
Type string
ParentID uuid.UUID
Level int
}
type TreeQuery struct {
WithItems bool `json:"withItems"`
}
func (lr *LocationRepository) Tree(ctx context.Context, GID uuid.UUID, tq TreeQuery) ([]TreeItem, error) {
query := `
WITH recursive location_tree(id, NAME, location_children, level, node_type) AS
(
SELECT id,
NAME,
location_children,
0 AS level,
'location' AS node_type
FROM locations
WHERE location_children IS NULL
AND group_locations = ?
UNION ALL
SELECT c.id,
c.NAME,
c.location_children,
level + 1,
'location' AS node_type
FROM locations c
JOIN location_tree p
ON c.location_children = p.id
WHERE level < 10 -- prevent infinite loop & excessive recursion
{{ WITH_ITEMS }}
)
SELECT id,
NAME,
level,
location_children,
node_type
FROM location_tree
ORDER BY level,
node_type DESC, -- sort locations before items
lower(NAME)`
if tq.WithItems {
itemQuery := `
UNION ALL
SELECT i.id,
i.name,
location_items as location_children,
level + 1,
'item' AS node_type
FROM items i
JOIN location_tree p
ON i.location_items = p.id
WHERE level < 10 -- prevent infinite loop & excessive recursion`
query = strings.ReplaceAll(query, "{{ WITH_ITEMS }}", itemQuery)
} else {
query = strings.ReplaceAll(query, "{{ WITH_ITEMS }}", "")
}
rows, err := lr.db.Sql().QueryContext(ctx, query, GID)
if err != nil {
return nil, err
}
defer rows.Close()
var locations []FlatTreeItem
for rows.Next() {
var location FlatTreeItem
if err := rows.Scan(&location.ID, &location.Name, &location.Level, &location.ParentID, &location.Type); err != nil {
return nil, err
}
locations = append(locations, location)
}
if err := rows.Err(); err != nil {
return nil, err
}
return ConvertLocationsToTree(locations), nil
}
func ConvertLocationsToTree(locations []FlatTreeItem) []TreeItem {
locationMap := make(map[uuid.UUID]*TreeItem, len(locations))
var rootIds []uuid.UUID
for _, location := range locations {
loc := &TreeItem{
ID: location.ID,
Name: location.Name,
Type: location.Type,
Children: []*TreeItem{},
}
locationMap[location.ID] = loc
if location.ParentID != uuid.Nil {
parent, ok := locationMap[location.ParentID]
if ok {
parent.Children = append(parent.Children, loc)
}
} else {
rootIds = append(rootIds, location.ID)
}
}
roots := make([]TreeItem, 0, len(rootIds))
for _, id := range rootIds {
roots = append(roots, *locationMap[id])
}
return roots
}