API stuff for entity types

This commit is contained in:
Matthew Kilgore
2025-09-10 21:34:39 -04:00
parent 5456390b63
commit 3f2ae368ba
12 changed files with 658 additions and 13214 deletions

View File

@@ -0,0 +1,81 @@
package v1
import (
"net/http"
"github.com/google/uuid"
"github.com/hay-kot/httpkit/errchain"
"github.com/sysadminsmedia/homebox/backend/internal/core/services"
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
"github.com/sysadminsmedia/homebox/backend/internal/web/adapters"
)
// HandleEntityTypesGetAll godoc
//
// @Summary Query All Entity Types
// @Tags EntityTypes
// @Produce json
// @Success 200 {array} repo.EntityType[]
// @Router /v1/entitytype [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleEntityTypesGetAll() errchain.HandlerFunc {
fn := func(r *http.Request) ([]repo.EntityType, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.EntityType.GetEntityTypesByGroupID(auth, auth.GID)
}
return adapters.Command(fn, http.StatusOK)
}
// HandleEntityTypeGetOne godoc
//
// @Summary Get One Entity Type
// @Tags EntityTypes
// @Produce json
// @Param id path string true "Entity Type ID"
// @Success 200 {object} repo.EntityType
// @Router /v1/entitytype/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleEntityTypeGetOne() errchain.HandlerFunc {
fn := func(r *http.Request, entityTypeID uuid.UUID) (repo.EntityType, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.EntityType.GetOneByGroup(auth, auth.GID, entityTypeID)
}
return adapters.CommandID("id", fn, http.StatusOK)
}
// HandleEntityTypeCreate godoc
//
// @Summary Create Entity Type
// @Tags EntityTypes
// @Accept json
// @Produce json
// @Param payload body repo.EntityTypeCreate true "Entity Type Data"
// @Success 201 {object} repo.EntityType
// @Router /v1/entitytype [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleEntityTypeCreate() errchain.HandlerFunc {
fn := func(r *http.Request, body repo.EntityTypeCreate) (repo.EntityType, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.EntityType.CreateEntityType(auth, auth.GID, body)
}
return adapters.Action(fn, http.StatusCreated)
}
// HandleEntityTypeUpdate godoc
//
// @Summary Update Entity Type
// @Tags EntityTypes
// @Accept json
// @Produce json
// @Param id path string true "Entity Type ID"
// @Param payload body repo.EntityTypeUpdate true "Entity Type Data"
// @Success 200 {object} repo.EntityType
// @Router /v1/entitytype/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleEntityTypeUpdate() errchain.HandlerFunc {
fn := func(r *http.Request, entityTypeID uuid.UUID, body repo.EntityTypeUpdate) (repo.EntityType, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.EntityType.UpdateEntityType(auth, auth.GID, entityTypeID, body)
}
return adapters.ActionID("id", fn, http.StatusOK)
}

View File

@@ -185,6 +185,12 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
// Reporting Services
r.Get("/reporting/bill-of-materials", chain.ToHandlerFunc(v1Ctrl.HandleBillOfMaterialsExport(), userMW...))
// Entity Types
r.Get("/entitytype", chain.ToHandlerFunc(v1Ctrl.HandleEntityTypesGetAll(), userMW...))
r.Post("/entitytype", chain.ToHandlerFunc(v1Ctrl.HandleEntityTypeCreate(), userMW...))
r.Get("/entitytype/{id}", chain.ToHandlerFunc(v1Ctrl.HandleEntityTypeGetOne(), userMW...))
r.Put("/entitytype/{id}", chain.ToHandlerFunc(v1Ctrl.HandleEntityTypeUpdate(), userMW...))
r.NotFound(http.NotFound)
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -244,9 +244,10 @@ definitions:
$ref: '#/definitions/ent.Entity'
type: array
entity:
allOf:
- $ref: '#/definitions/ent.Entity'
description: Entity holds the value of the entity edge.
items:
$ref: '#/definitions/ent.Entity'
type: array
fields:
description: Fields holds the value of the fields edge.
items:
@@ -686,6 +687,77 @@ definitions:
copyPrefix:
type: string
type: object
repo.EntityAttachment:
properties:
createdAt:
type: string
id:
type: string
mimeType:
type: string
path:
type: string
primary:
type: boolean
thumbnail:
$ref: '#/definitions/ent.Attachment'
title:
type: string
type:
type: string
updatedAt:
type: string
type: object
repo.EntityAttachmentUpdate:
properties:
primary:
type: boolean
title:
type: string
type:
type: string
type: object
repo.EntityType:
properties:
color:
type: string
description:
type: string
icon:
type: string
isLocation:
type: boolean
name:
type: string
type: object
repo.EntityTypeCreate:
properties:
color:
type: string
description:
type: string
icon:
type: string
isLocation:
type: boolean
name:
type: string
required:
- isLocation
- name
type: object
repo.EntityTypeUpdate:
properties:
color:
type: string
description:
type: string
icon:
type: string
name:
minLength: 1
type: string
type: object
repo.Group:
properties:
createdAt:
@@ -721,41 +793,13 @@ definitions:
name:
type: string
type: object
repo.ItemAttachment:
properties:
createdAt:
type: string
id:
type: string
mimeType:
type: string
path:
type: string
primary:
type: boolean
thumbnail:
$ref: '#/definitions/ent.Attachment'
title:
type: string
type:
type: string
updatedAt:
type: string
type: object
repo.ItemAttachmentUpdate:
properties:
primary:
type: boolean
title:
type: string
type:
type: string
type: object
repo.ItemCreate:
properties:
description:
maxLength: 1000
type: string
entityType:
type: string
labelIds:
items:
type: string
@@ -773,6 +817,7 @@ definitions:
quantity:
type: integer
required:
- entityType
- name
type: object
repo.ItemField:
@@ -799,12 +844,14 @@ definitions:
type: string
attachments:
items:
$ref: '#/definitions/repo.ItemAttachment'
$ref: '#/definitions/repo.EntityAttachment'
type: array
createdAt:
type: string
description:
type: string
entityType:
type: string
fields:
items:
$ref: '#/definitions/repo.ItemField'
@@ -906,6 +953,8 @@ definitions:
type: string
description:
type: string
entityType:
type: string
id:
type: string
imageId:
@@ -957,6 +1006,8 @@ definitions:
description:
maxLength: 1000
type: string
entityType:
type: string
fields:
items:
$ref: '#/definitions/repo.ItemField'
@@ -1024,6 +1075,7 @@ definitions:
warrantyExpires:
type: string
required:
- entityType
- name
type: object
repo.LabelCreate:
@@ -1074,11 +1126,15 @@ definitions:
properties:
description:
type: string
entityType:
type: string
name:
type: string
parentId:
type: string
x-nullable: true
required:
- entityType
type: object
repo.LocationOut:
properties:
@@ -1090,6 +1146,8 @@ definitions:
type: string
description:
type: string
entityType:
type: string
id:
type: string
name:
@@ -1107,6 +1165,8 @@ definitions:
type: string
description:
type: string
entityType:
type: string
id:
type: string
itemCount:
@@ -1122,6 +1182,8 @@ definitions:
type: string
description:
type: string
entityType:
type: string
id:
type: string
name:
@@ -1133,6 +1195,8 @@ definitions:
properties:
description:
type: string
entityType:
type: string
id:
type: string
name:
@@ -1140,6 +1204,8 @@ definitions:
parentId:
type: string
x-nullable: true
required:
- entityType
type: object
repo.MaintenanceEntry:
properties:
@@ -1697,7 +1763,7 @@ paths:
name: payload
required: true
schema:
$ref: '#/definitions/repo.ItemAttachmentUpdate'
$ref: '#/definitions/repo.EntityAttachmentUpdate'
responses:
"200":
description: OK
@@ -1766,6 +1832,91 @@ paths:
summary: Create Maintenance Entry
tags:
- Item Maintenance
/v1/entitytype:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/repo.EntityType'
type: array
security:
- Bearer: []
summary: Query All Entity Types
tags:
- EntityTypes
post:
consumes:
- application/json
parameters:
- description: Entity Type Data
in: body
name: payload
required: true
schema:
$ref: '#/definitions/repo.EntityTypeCreate'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/repo.EntityType'
security:
- Bearer: []
summary: Create Entity Type
tags:
- EntityTypes
/v1/entitytype/{id}:
get:
parameters:
- description: Entity Type ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/repo.EntityType'
security:
- Bearer: []
summary: Get One Entity Type
tags:
- EntityTypes
put:
consumes:
- application/json
parameters:
- description: Entity Type ID
in: path
name: id
required: true
type: string
- description: Entity Type Data
in: body
name: payload
required: true
schema:
$ref: '#/definitions/repo.EntityTypeUpdate'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/repo.EntityType'
security:
- Bearer: []
summary: Update Entity Type
tags:
- EntityTypes
/v1/groups:
get:
produces:
@@ -2163,7 +2314,7 @@ paths:
name: payload
required: true
schema:
$ref: '#/definitions/repo.ItemAttachmentUpdate'
$ref: '#/definitions/repo.EntityAttachmentUpdate'
responses:
"200":
description: OK

View File

@@ -105,7 +105,7 @@ func (svc *UserService) RegisterUser(ctx context.Context, data UserRegistration)
}
log.Debug().Msg("creating default entity types")
err := svc.repos.EntityType.CreateDefaultEntities(ctx, usr.GroupID)
err := svc.repos.EntityType.CreateDefaultEntityTypes(ctx, usr.GroupID)
if err != nil {
return repo.UserOut{}, err
}

View File

@@ -2,9 +2,12 @@ package repo
import (
"context"
"github.com/google/uuid"
"github.com/sysadminsmedia/homebox/backend/internal/core/services/reporting/eventbus"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/entitytype"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/group"
)
type EntityTypeRepository struct {
@@ -12,7 +15,32 @@ type EntityTypeRepository struct {
bus *eventbus.EventBus
}
func (e *EntityTypeRepository) CreateDefaultEntities(ctx context.Context, gid uuid.UUID) error {
type (
EntityType struct {
Name string `json:"name"`
IsLocation bool `json:"isLocation"`
Description string `json:"description" extension:"x-nullable"`
Icon string `json:"icon" extension:"x-nullable"`
Color string `json:"color" extension:"x-nullable"`
}
EntityTypeCreate struct {
Name string `json:"name" validate:"required"`
IsLocation bool `json:"isLocation" validate:"required"`
Description string `json:"description" extension:"x-nullable"`
Icon string `json:"icon" extension:"x-nullable"`
Color string `json:"color" extension:"x-nullable"`
}
EntityTypeUpdate struct {
Name string `json:"name" validate:"omitempty,min=1"`
Description string `json:"description" extension:"x-nullable"`
Icon string `json:"icon" extension:"x-nullable"`
Color string `json:"color" extension:"x-nullable"`
}
)
func (e *EntityTypeRepository) CreateDefaultEntityTypes(ctx context.Context, gid uuid.UUID) error {
_, err := e.db.EntityType.Create().SetIsLocation(true).SetName("Location").SetGroupID(gid).Save(ctx)
if err != nil {
return err
@@ -23,3 +51,85 @@ func (e *EntityTypeRepository) CreateDefaultEntities(ctx context.Context, gid uu
}
return nil
}
func (e *EntityTypeRepository) GetOneByGroup(ctx context.Context, gid uuid.UUID, id uuid.UUID) (EntityType, error) {
entityType, err := e.db.EntityType.Query().Where(entitytype.HasGroupWith(group.ID(gid)), entitytype.ID(id)).Only(ctx)
if err != nil {
return EntityType{}, err
}
return EntityType{
Name: entityType.Name,
IsLocation: entityType.IsLocation,
Description: entityType.Description,
Icon: entityType.Icon,
Color: entityType.Color,
}, nil
}
func (e *EntityTypeRepository) GetEntityTypesByGroupID(ctx context.Context, gid uuid.UUID) ([]EntityType, error) {
entityTypes, err := e.db.EntityType.Query().Where(entitytype.HasGroupWith(group.ID(gid))).All(ctx)
if err != nil {
return nil, err
}
result := make([]EntityType, 0, len(entityTypes))
for _, et := range entityTypes {
result = append(result, EntityType{
Name: et.Name,
IsLocation: et.IsLocation,
Description: et.Description,
Icon: et.Icon,
Color: et.Color,
})
}
return result, nil
}
func (e *EntityTypeRepository) CreateEntityType(ctx context.Context, gid uuid.UUID, data EntityTypeCreate) (EntityType, error) {
entityType, err := e.db.EntityType.Create().
SetGroupID(gid).
SetName(data.Name).
SetIsLocation(data.IsLocation).
SetDescription(data.Description).
SetIcon(data.Icon).
SetColor(data.Color).
Save(ctx)
if err != nil {
return EntityType{}, err
}
return EntityType{
Name: entityType.Name,
IsLocation: entityType.IsLocation,
Description: entityType.Description,
Icon: entityType.Icon,
Color: entityType.Color,
}, nil
}
func (e *EntityTypeRepository) UpdateEntityType(ctx context.Context, gid uuid.UUID, id uuid.UUID, data EntityTypeUpdate) (EntityType, error) {
et, err := e.GetOneByGroup(ctx, gid, id)
if err != nil {
return EntityType{}, err
}
update := e.db.EntityType.Update().Where(entitytype.ID(id))
if data.Name != "" {
update = update.SetName(data.Name)
et.Name = data.Name
}
if data.Description != "" {
update = update.SetDescription(data.Description)
et.Description = data.Description
}
if data.Icon != "" {
update = update.SetIcon(data.Icon)
et.Icon = data.Icon
}
if data.Color != "" {
update = update.SetColor(data.Color)
et.Color = data.Color
}
_, err = update.Save(ctx)
if err != nil {
return EntityType{}, err
}
return et, nil
}

View File

@@ -73,6 +73,7 @@ type (
Quantity int `json:"quantity"`
Description string `json:"description" validate:"max=1000"`
AssetID AssetID `json:"-"`
EntityType uuid.UUID `json:"entityType" validate:"required,uuid"`
// Edges
LocationID uuid.UUID `json:"locationId"`
@@ -89,6 +90,7 @@ type (
Insured bool `json:"insured"`
Archived bool `json:"archived"`
SyncChildItemsLocations bool `json:"syncChildItemsLocations"`
EntityType uuid.UUID `json:"entityType" validate:"required,uuid"`
// Edges
LocationID uuid.UUID `json:"locationId"`
@@ -137,6 +139,7 @@ type (
Archived bool `json:"archived"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
EntityType uuid.UUID `json:"entityType"`
PurchasePrice float64 `json:"purchasePrice"`
@@ -219,6 +222,11 @@ func mapItemSummary(item *ent.Entity) ItemSummary {
}
}
var typeID uuid.UUID
if item.Edges.Type != nil {
typeID = item.Edges.Type.ID
}
return ItemSummary{
ID: item.ID,
AssetID: AssetID(item.AssetID),
@@ -230,6 +238,7 @@ func mapItemSummary(item *ent.Entity) ItemSummary {
UpdatedAt: item.UpdatedAt,
Archived: item.Archived,
PurchasePrice: item.PurchasePrice,
EntityType: typeID,
// Edges
Location: location,
@@ -327,6 +336,7 @@ func (e *ItemsRepository) getOne(ctx context.Context, where ...predicate.Entity)
WithGroup().
WithParent().
WithAttachments().
WithType().
Only(ctx),
)
}
@@ -496,6 +506,7 @@ func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q Ite
qb = qb.
WithLabel().
WithLocation().
WithType().
WithAttachments(func(aq *ent.AttachmentQuery) {
aq.Where(
attachment.Primary(true),
@@ -542,6 +553,7 @@ func (e *ItemsRepository) QueryByAssetID(ctx context.Context, gid uuid.UUID, ass
qb.Order(ent.Asc(entity.FieldName)).
WithLabel().
WithLocation().
WithType().
All(ctx),
)
if err != nil {
@@ -563,6 +575,7 @@ func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]ItemOut,
WithLabel().
WithLocation().
WithFields().
WithType().
All(ctx))
}
@@ -613,6 +626,7 @@ func (e *ItemsRepository) Create(ctx context.Context, gid uuid.UUID, data ItemCr
SetDescription(data.Description).
SetGroupID(gid).
SetAssetID(int(data.AssetID)).
SetTypeID(data.EntityType).
SetLocationID(data.LocationID)
if data.ParentID != uuid.Nil {
@@ -680,6 +694,7 @@ func (e *ItemsRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data
SetWarrantyDetails(data.WarrantyDetails).
SetQuantity(data.Quantity).
SetAssetID(int(data.AssetID)).
SetTypeID(data.EntityType).
SetSyncChildEntitiesLocations(data.SyncChildItemsLocations)
currentLabels, err := e.db.Entity.Query().Where(entity.ID(data.ID), entity.HasTypeWith(entitytype.IsLocationEQ(false))).QueryLabel().All(ctx)
@@ -1081,6 +1096,7 @@ func (e *ItemsRepository) Duplicate(ctx context.Context, gid, id uuid.UUID, opti
SetNotes(originalItem.Notes).
SetInsured(originalItem.Insured).
SetArchived(originalItem.Archived).
SetTypeID(originalItem.EntityType).
SetSyncChildEntitiesLocations(originalItem.SyncChildItemsLocations)
if originalItem.Parent != nil {

View File

@@ -24,6 +24,7 @@ type (
Name string `json:"name"`
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable"`
Description string `json:"description"`
EntityType uuid.UUID `json:"entityType" validate:"required"`
}
LocationUpdate struct {
@@ -31,6 +32,7 @@ type (
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
EntityType uuid.UUID `json:"entityType" validate:"required"`
}
LocationSummary struct {
@@ -39,6 +41,7 @@ type (
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
EntityType uuid.UUID `json:"entityType"`
}
LocationOutCount struct {
@@ -55,12 +58,17 @@ type (
)
func mapLocationSummary(location *ent.Entity) LocationSummary {
var typeID uuid.UUID
if location.Edges.Type != nil {
typeID = location.Edges.Type.ID
}
return LocationSummary{
ID: location.ID,
Name: location.Name,
Description: location.Description,
CreatedAt: location.CreatedAt,
UpdatedAt: location.UpdatedAt,
EntityType: typeID,
}
}
@@ -169,6 +177,7 @@ func (r *LocationRepository) getOne(ctx context.Context, where ...predicate.Enti
Where(entity.HasTypeWith(entitytype.IsLocationEQ(true))).
WithGroup().
WithParent().
WithType().
WithChildren(func(lq *ent.EntityQuery) {
lq.Order(entity.ByName())
}).
@@ -188,7 +197,7 @@ func (r *LocationRepository) Create(ctx context.Context, gid uuid.UUID, data Loc
SetName(data.Name).
SetDescription(data.Description).
SetGroupID(gid).
SetType(r.db.EntityType.Query().Where(entitytype.IsLocationEQ(true)).FirstX(ctx))
SetTypeID(data.EntityType)
if data.ParentID != uuid.Nil {
q.SetParentID(data.ParentID)
@@ -208,7 +217,8 @@ func (r *LocationRepository) update(ctx context.Context, data LocationUpdate, wh
q := r.db.Entity.Update().
Where(where...).
SetName(data.Name).
SetDescription(data.Description)
SetDescription(data.Description).
SetTypeID(data.EntityType)
if data.ParentID != uuid.Nil {
q.SetParentID(data.ParentID)