mirror of
https://github.com/hay-kot/homebox.git
synced 2024-11-16 13:48:44 +00:00
feat: asset tags/ids (#142)
* add schema * run db migration * bulk seed asset IDs * breaking: update runtime options * conditionally increment asset IDs * update API endpoints * fix import asset id assignment * refactor display + marshal/unmarshal * add docs page * add to form field * hide 000-000 values * update ENV vars
This commit is contained in:
parent
976f68252d
commit
6dc2ae1bea
32 changed files with 905 additions and 72 deletions
35
backend/app/api/handlers/v1/v1_ctrl_actions.go
Normal file
35
backend/app/api/handlers/v1/v1_ctrl_actions.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/hay-kot/homebox/backend/internal/core/services"
|
||||
"github.com/hay-kot/homebox/backend/internal/sys/validate"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type EnsureAssetIDResult struct {
|
||||
Completed int `json:"completed"`
|
||||
}
|
||||
|
||||
// HandleGroupInvitationsCreate godoc
|
||||
// @Summary Get the current user
|
||||
// @Tags Group
|
||||
// @Produce json
|
||||
// @Success 200 {object} EnsureAssetIDResult
|
||||
// @Router /v1/actions/ensure-asset-ids [Post]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleEnsureAssetID() server.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx := services.NewContext(r.Context())
|
||||
|
||||
totalCompleted, err := ctrl.svc.Items.EnsureAssetID(ctx, ctx.GID)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to ensure asset id")
|
||||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return server.Respond(w, http.StatusOK, EnsureAssetIDResult{Completed: totalCompleted})
|
||||
}
|
||||
}
|
|
@ -65,8 +65,8 @@ func (ctrl *V1Controller) HandleItemsCreate() server.HandlerFunc {
|
|||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
user := services.UseUserCtx(r.Context())
|
||||
item, err := ctrl.repo.Items.Create(r.Context(), user.GroupID, createData)
|
||||
ctx := services.NewContext(r.Context())
|
||||
item, err := ctrl.svc.Items.Create(ctx, createData)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to create item")
|
||||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||
|
|
|
@ -112,7 +112,10 @@ func run(cfg *config.Config) error {
|
|||
|
||||
app.db = c
|
||||
app.repos = repo.New(c, cfg.Storage.Data)
|
||||
app.services = services.New(app.repos)
|
||||
app.services = services.New(
|
||||
app.repos,
|
||||
services.WithAutoIncrementAssetID(cfg.Options.AutoIncrementAssetID),
|
||||
)
|
||||
|
||||
// =========================================================================
|
||||
// Start Server\
|
||||
|
|
|
@ -51,7 +51,7 @@ func (a *app) mountRoutes(repos *repo.AllRepos) {
|
|||
a.services,
|
||||
a.repos,
|
||||
v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize),
|
||||
v1.WithRegistration(a.conf.AllowRegistration),
|
||||
v1.WithRegistration(a.conf.Options.AllowRegistration),
|
||||
v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode
|
||||
)
|
||||
|
||||
|
@ -82,6 +82,8 @@ func (a *app) mountRoutes(repos *repo.AllRepos) {
|
|||
a.server.Get(v1Base("/groups"), v1Ctrl.HandleGroupGet(), a.mwAuthToken)
|
||||
a.server.Put(v1Base("/groups"), v1Ctrl.HandleGroupUpdate(), a.mwAuthToken)
|
||||
|
||||
a.server.Post(v1Base("/actions/ensure-asset-ids"), v1Ctrl.HandleEnsureAssetID(), a.mwAuthToken)
|
||||
|
||||
a.server.Get(v1Base("/locations"), v1Ctrl.HandleLocationGetAll(), a.mwAuthToken)
|
||||
a.server.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate(), a.mwAuthToken)
|
||||
a.server.Get(v1Base("/locations/{id}"), v1Ctrl.HandleLocationGet(), a.mwAuthToken)
|
||||
|
|
|
@ -21,6 +21,30 @@ const docTemplate = `{
|
|||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/v1/actions/ensure-asset-ids": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Group"
|
||||
],
|
||||
"summary": "Get the current user",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.EnsureAssetIDResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/groups": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -1326,6 +1350,10 @@ const docTemplate = `{
|
|||
"archived": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"assetId": {
|
||||
"type": "string",
|
||||
"example": "0"
|
||||
},
|
||||
"attachments": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -1479,6 +1507,9 @@ const docTemplate = `{
|
|||
"archived": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"assetId": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -1891,6 +1922,14 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.EnsureAssetIDResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"completed": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.GroupInvitation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -13,6 +13,30 @@
|
|||
},
|
||||
"basePath": "/api",
|
||||
"paths": {
|
||||
"/v1/actions/ensure-asset-ids": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Group"
|
||||
],
|
||||
"summary": "Get the current user",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.EnsureAssetIDResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/groups": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -1318,6 +1342,10 @@
|
|||
"archived": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"assetId": {
|
||||
"type": "string",
|
||||
"example": "0"
|
||||
},
|
||||
"attachments": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -1471,6 +1499,9 @@
|
|||
"archived": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"assetId": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -1883,6 +1914,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.EnsureAssetIDResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"completed": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.GroupInvitation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -98,6 +98,9 @@ definitions:
|
|||
properties:
|
||||
archived:
|
||||
type: boolean
|
||||
assetId:
|
||||
example: "0"
|
||||
type: string
|
||||
attachments:
|
||||
items:
|
||||
$ref: '#/definitions/repo.ItemAttachment'
|
||||
|
@ -204,6 +207,8 @@ definitions:
|
|||
properties:
|
||||
archived:
|
||||
type: boolean
|
||||
assetId:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
fields:
|
||||
|
@ -477,6 +482,11 @@ definitions:
|
|||
new:
|
||||
type: string
|
||||
type: object
|
||||
v1.EnsureAssetIDResult:
|
||||
properties:
|
||||
completed:
|
||||
type: integer
|
||||
type: object
|
||||
v1.GroupInvitation:
|
||||
properties:
|
||||
expiresAt:
|
||||
|
@ -516,6 +526,20 @@ info:
|
|||
title: Go API Templates
|
||||
version: "1.0"
|
||||
paths:
|
||||
/v1/actions/ensure-asset-ids:
|
||||
post:
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/v1.EnsureAssetIDResult'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Get the current user
|
||||
tags:
|
||||
- Group
|
||||
/v1/groups:
|
||||
get:
|
||||
produces:
|
||||
|
|
|
@ -8,17 +8,38 @@ type AllServices struct {
|
|||
Items *ItemService
|
||||
}
|
||||
|
||||
func New(repos *repo.AllRepos) *AllServices {
|
||||
type OptionsFunc func(*options)
|
||||
|
||||
type options struct {
|
||||
autoIncrementAssetID bool
|
||||
}
|
||||
|
||||
func WithAutoIncrementAssetID(v bool) func(*options) {
|
||||
return func(o *options) {
|
||||
o.autoIncrementAssetID = v
|
||||
}
|
||||
}
|
||||
|
||||
func New(repos *repo.AllRepos, opts ...OptionsFunc) *AllServices {
|
||||
if repos == nil {
|
||||
panic("repos cannot be nil")
|
||||
}
|
||||
|
||||
options := &options{
|
||||
autoIncrementAssetID: true,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
return &AllServices{
|
||||
User: &UserService{repos},
|
||||
Group: &GroupService{repos},
|
||||
Items: &ItemService{
|
||||
repo: repos,
|
||||
at: attachmentTokens{},
|
||||
autoIncrementAssetID: options.autoIncrementAssetID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,49 @@ type ItemService struct {
|
|||
// at is a map of tokens to attachment IDs. This is used to store the attachment ID
|
||||
// for issued URLs
|
||||
at attachmentTokens
|
||||
|
||||
autoIncrementAssetID bool
|
||||
}
|
||||
|
||||
func (svc *ItemService) Create(ctx Context, item repo.ItemCreate) (repo.ItemOut, error) {
|
||||
if svc.autoIncrementAssetID {
|
||||
highest, err := svc.repo.Items.GetHighestAssetID(ctx, ctx.GID)
|
||||
if err != nil {
|
||||
return repo.ItemOut{}, err
|
||||
}
|
||||
|
||||
item.AssetID = repo.AssetID(highest + 1)
|
||||
}
|
||||
|
||||
return svc.repo.Items.Create(ctx, ctx.GID, item)
|
||||
}
|
||||
|
||||
func (svc *ItemService) EnsureAssetID(ctx context.Context, GID uuid.UUID) (int, error) {
|
||||
items, err := svc.repo.Items.GetAllZeroAssetID(ctx, GID)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
highest, err := svc.repo.Items.GetHighestAssetID(ctx, GID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
finished := 0
|
||||
for _, item := range items {
|
||||
highest++
|
||||
|
||||
err = svc.repo.Items.SetAssetID(ctx, GID, item.ID, repo.AssetID(highest))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
finished++
|
||||
}
|
||||
|
||||
return finished, nil
|
||||
}
|
||||
func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data [][]string) (int, error) {
|
||||
loaded := []csvRow{}
|
||||
|
||||
|
@ -114,6 +155,14 @@ func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data [][]s
|
|||
}
|
||||
}
|
||||
|
||||
highest := repo.AssetID(-1)
|
||||
if svc.autoIncrementAssetID {
|
||||
highest, err = svc.repo.Items.GetHighestAssetID(ctx, GID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// Create the items
|
||||
var count int
|
||||
for _, row := range loaded {
|
||||
|
@ -139,13 +188,20 @@ func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data [][]s
|
|||
Str("location", row.Location).
|
||||
Msgf("Creating Item: %s", row.Item.Name)
|
||||
|
||||
result, err := svc.repo.Items.Create(ctx, GID, repo.ItemCreate{
|
||||
data := repo.ItemCreate{
|
||||
ImportRef: row.Item.ImportRef,
|
||||
Name: row.Item.Name,
|
||||
Description: row.Item.Description,
|
||||
LabelIDs: labelIDs,
|
||||
LocationID: locationID,
|
||||
})
|
||||
}
|
||||
|
||||
if svc.autoIncrementAssetID {
|
||||
highest++
|
||||
data.AssetID = highest
|
||||
}
|
||||
|
||||
result, err := svc.repo.Items.Create(ctx, GID, data)
|
||||
|
||||
if err != nil {
|
||||
return count, err
|
||||
|
@ -156,6 +212,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data [][]s
|
|||
// Edges
|
||||
LocationID: locationID,
|
||||
LabelIDs: labelIDs,
|
||||
AssetID: data.AssetID,
|
||||
|
||||
// General Fields
|
||||
ID: result.ID,
|
||||
|
|
|
@ -37,6 +37,8 @@ type Item struct {
|
|||
Insured bool `json:"insured,omitempty"`
|
||||
// Archived holds the value of the "archived" field.
|
||||
Archived bool `json:"archived,omitempty"`
|
||||
// AssetID holds the value of the "asset_id" field.
|
||||
AssetID int `json:"asset_id,omitempty"`
|
||||
// SerialNumber holds the value of the "serial_number" field.
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
// ModelNumber holds the value of the "model_number" field.
|
||||
|
@ -176,7 +178,7 @@ func (*Item) scanValues(columns []string) ([]any, error) {
|
|||
values[i] = new(sql.NullBool)
|
||||
case item.FieldPurchasePrice, item.FieldSoldPrice:
|
||||
values[i] = new(sql.NullFloat64)
|
||||
case item.FieldQuantity:
|
||||
case item.FieldQuantity, item.FieldAssetID:
|
||||
values[i] = new(sql.NullInt64)
|
||||
case item.FieldName, item.FieldDescription, item.FieldImportRef, item.FieldNotes, item.FieldSerialNumber, item.FieldModelNumber, item.FieldManufacturer, item.FieldWarrantyDetails, item.FieldPurchaseFrom, item.FieldSoldTo, item.FieldSoldNotes:
|
||||
values[i] = new(sql.NullString)
|
||||
|
@ -265,6 +267,12 @@ func (i *Item) assignValues(columns []string, values []any) error {
|
|||
} else if value.Valid {
|
||||
i.Archived = value.Bool
|
||||
}
|
||||
case item.FieldAssetID:
|
||||
if value, ok := values[j].(*sql.NullInt64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field asset_id", values[j])
|
||||
} else if value.Valid {
|
||||
i.AssetID = int(value.Int64)
|
||||
}
|
||||
case item.FieldSerialNumber:
|
||||
if value, ok := values[j].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field serial_number", values[j])
|
||||
|
@ -454,6 +462,9 @@ func (i *Item) String() string {
|
|||
builder.WriteString("archived=")
|
||||
builder.WriteString(fmt.Sprintf("%v", i.Archived))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("asset_id=")
|
||||
builder.WriteString(fmt.Sprintf("%v", i.AssetID))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("serial_number=")
|
||||
builder.WriteString(i.SerialNumber)
|
||||
builder.WriteString(", ")
|
||||
|
|
|
@ -31,6 +31,8 @@ const (
|
|||
FieldInsured = "insured"
|
||||
// FieldArchived holds the string denoting the archived field in the database.
|
||||
FieldArchived = "archived"
|
||||
// FieldAssetID holds the string denoting the asset_id field in the database.
|
||||
FieldAssetID = "asset_id"
|
||||
// FieldSerialNumber holds the string denoting the serial_number field in the database.
|
||||
FieldSerialNumber = "serial_number"
|
||||
// FieldModelNumber holds the string denoting the model_number field in the database.
|
||||
|
@ -128,6 +130,7 @@ var Columns = []string{
|
|||
FieldQuantity,
|
||||
FieldInsured,
|
||||
FieldArchived,
|
||||
FieldAssetID,
|
||||
FieldSerialNumber,
|
||||
FieldModelNumber,
|
||||
FieldManufacturer,
|
||||
|
@ -193,6 +196,8 @@ var (
|
|||
DefaultInsured bool
|
||||
// DefaultArchived holds the default value on creation for the "archived" field.
|
||||
DefaultArchived bool
|
||||
// DefaultAssetID holds the default value on creation for the "asset_id" field.
|
||||
DefaultAssetID int
|
||||
// SerialNumberValidator is a validator for the "serial_number" field. It is called by the builders before save.
|
||||
SerialNumberValidator func(string) error
|
||||
// ModelNumberValidator is a validator for the "model_number" field. It is called by the builders before save.
|
||||
|
|
|
@ -145,6 +145,13 @@ func Archived(v bool) predicate.Item {
|
|||
})
|
||||
}
|
||||
|
||||
// AssetID applies equality check predicate on the "asset_id" field. It's identical to AssetIDEQ.
|
||||
func AssetID(v int) predicate.Item {
|
||||
return predicate.Item(func(s *sql.Selector) {
|
||||
s.Where(sql.EQ(s.C(FieldAssetID), v))
|
||||
})
|
||||
}
|
||||
|
||||
// SerialNumber applies equality check predicate on the "serial_number" field. It's identical to SerialNumberEQ.
|
||||
func SerialNumber(v string) predicate.Item {
|
||||
return predicate.Item(func(s *sql.Selector) {
|
||||
|
@ -894,6 +901,70 @@ func ArchivedNEQ(v bool) predicate.Item {
|
|||
})
|
||||
}
|
||||
|
||||
// AssetIDEQ applies the EQ predicate on the "asset_id" field.
|
||||
func AssetIDEQ(v int) predicate.Item {
|
||||
return predicate.Item(func(s *sql.Selector) {
|
||||
s.Where(sql.EQ(s.C(FieldAssetID), v))
|
||||
})
|
||||
}
|
||||
|
||||
// AssetIDNEQ applies the NEQ predicate on the "asset_id" field.
|
||||
func AssetIDNEQ(v int) predicate.Item {
|
||||
return predicate.Item(func(s *sql.Selector) {
|
||||
s.Where(sql.NEQ(s.C(FieldAssetID), v))
|
||||
})
|
||||
}
|
||||
|
||||
// AssetIDIn applies the In predicate on the "asset_id" field.
|
||||
func AssetIDIn(vs ...int) predicate.Item {
|
||||
v := make([]any, len(vs))
|
||||
for i := range v {
|
||||
v[i] = vs[i]
|
||||
}
|
||||
return predicate.Item(func(s *sql.Selector) {
|
||||
s.Where(sql.In(s.C(FieldAssetID), v...))
|
||||
})
|
||||
}
|
||||
|
||||
// AssetIDNotIn applies the NotIn predicate on the "asset_id" field.
|
||||
func AssetIDNotIn(vs ...int) predicate.Item {
|
||||
v := make([]any, len(vs))
|
||||
for i := range v {
|
||||
v[i] = vs[i]
|
||||
}
|
||||
return predicate.Item(func(s *sql.Selector) {
|
||||
s.Where(sql.NotIn(s.C(FieldAssetID), v...))
|
||||
})
|
||||
}
|
||||
|
||||
// AssetIDGT applies the GT predicate on the "asset_id" field.
|
||||
func AssetIDGT(v int) predicate.Item {
|
||||
return predicate.Item(func(s *sql.Selector) {
|
||||
s.Where(sql.GT(s.C(FieldAssetID), v))
|
||||
})
|
||||
}
|
||||
|
||||
// AssetIDGTE applies the GTE predicate on the "asset_id" field.
|
||||
func AssetIDGTE(v int) predicate.Item {
|
||||
return predicate.Item(func(s *sql.Selector) {
|
||||
s.Where(sql.GTE(s.C(FieldAssetID), v))
|
||||
})
|
||||
}
|
||||
|
||||
// AssetIDLT applies the LT predicate on the "asset_id" field.
|
||||
func AssetIDLT(v int) predicate.Item {
|
||||
return predicate.Item(func(s *sql.Selector) {
|
||||
s.Where(sql.LT(s.C(FieldAssetID), v))
|
||||
})
|
||||
}
|
||||
|
||||
// AssetIDLTE applies the LTE predicate on the "asset_id" field.
|
||||
func AssetIDLTE(v int) predicate.Item {
|
||||
return predicate.Item(func(s *sql.Selector) {
|
||||
s.Where(sql.LTE(s.C(FieldAssetID), v))
|
||||
})
|
||||
}
|
||||
|
||||
// SerialNumberEQ applies the EQ predicate on the "serial_number" field.
|
||||
func SerialNumberEQ(v string) predicate.Item {
|
||||
return predicate.Item(func(s *sql.Selector) {
|
||||
|
|
|
@ -144,6 +144,20 @@ func (ic *ItemCreate) SetNillableArchived(b *bool) *ItemCreate {
|
|||
return ic
|
||||
}
|
||||
|
||||
// SetAssetID sets the "asset_id" field.
|
||||
func (ic *ItemCreate) SetAssetID(i int) *ItemCreate {
|
||||
ic.mutation.SetAssetID(i)
|
||||
return ic
|
||||
}
|
||||
|
||||
// SetNillableAssetID sets the "asset_id" field if the given value is not nil.
|
||||
func (ic *ItemCreate) SetNillableAssetID(i *int) *ItemCreate {
|
||||
if i != nil {
|
||||
ic.SetAssetID(*i)
|
||||
}
|
||||
return ic
|
||||
}
|
||||
|
||||
// SetSerialNumber sets the "serial_number" field.
|
||||
func (ic *ItemCreate) SetSerialNumber(s string) *ItemCreate {
|
||||
ic.mutation.SetSerialNumber(s)
|
||||
|
@ -546,6 +560,10 @@ func (ic *ItemCreate) defaults() {
|
|||
v := item.DefaultArchived
|
||||
ic.mutation.SetArchived(v)
|
||||
}
|
||||
if _, ok := ic.mutation.AssetID(); !ok {
|
||||
v := item.DefaultAssetID
|
||||
ic.mutation.SetAssetID(v)
|
||||
}
|
||||
if _, ok := ic.mutation.LifetimeWarranty(); !ok {
|
||||
v := item.DefaultLifetimeWarranty
|
||||
ic.mutation.SetLifetimeWarranty(v)
|
||||
|
@ -604,6 +622,9 @@ func (ic *ItemCreate) check() error {
|
|||
if _, ok := ic.mutation.Archived(); !ok {
|
||||
return &ValidationError{Name: "archived", err: errors.New(`ent: missing required field "Item.archived"`)}
|
||||
}
|
||||
if _, ok := ic.mutation.AssetID(); !ok {
|
||||
return &ValidationError{Name: "asset_id", err: errors.New(`ent: missing required field "Item.asset_id"`)}
|
||||
}
|
||||
if v, ok := ic.mutation.SerialNumber(); ok {
|
||||
if err := item.SerialNumberValidator(v); err != nil {
|
||||
return &ValidationError{Name: "serial_number", err: fmt.Errorf(`ent: validator failed for field "Item.serial_number": %w`, err)}
|
||||
|
@ -749,6 +770,14 @@ func (ic *ItemCreate) createSpec() (*Item, *sqlgraph.CreateSpec) {
|
|||
})
|
||||
_node.Archived = value
|
||||
}
|
||||
if value, ok := ic.mutation.AssetID(); ok {
|
||||
_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
|
||||
Type: field.TypeInt,
|
||||
Value: value,
|
||||
Column: item.FieldAssetID,
|
||||
})
|
||||
_node.AssetID = value
|
||||
}
|
||||
if value, ok := ic.mutation.SerialNumber(); ok {
|
||||
_spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
|
||||
Type: field.TypeString,
|
||||
|
|
|
@ -135,6 +135,27 @@ func (iu *ItemUpdate) SetNillableArchived(b *bool) *ItemUpdate {
|
|||
return iu
|
||||
}
|
||||
|
||||
// SetAssetID sets the "asset_id" field.
|
||||
func (iu *ItemUpdate) SetAssetID(i int) *ItemUpdate {
|
||||
iu.mutation.ResetAssetID()
|
||||
iu.mutation.SetAssetID(i)
|
||||
return iu
|
||||
}
|
||||
|
||||
// SetNillableAssetID sets the "asset_id" field if the given value is not nil.
|
||||
func (iu *ItemUpdate) SetNillableAssetID(i *int) *ItemUpdate {
|
||||
if i != nil {
|
||||
iu.SetAssetID(*i)
|
||||
}
|
||||
return iu
|
||||
}
|
||||
|
||||
// AddAssetID adds i to the "asset_id" field.
|
||||
func (iu *ItemUpdate) AddAssetID(i int) *ItemUpdate {
|
||||
iu.mutation.AddAssetID(i)
|
||||
return iu
|
||||
}
|
||||
|
||||
// SetSerialNumber sets the "serial_number" field.
|
||||
func (iu *ItemUpdate) SetSerialNumber(s string) *ItemUpdate {
|
||||
iu.mutation.SetSerialNumber(s)
|
||||
|
@ -816,6 +837,20 @@ func (iu *ItemUpdate) sqlSave(ctx context.Context) (n int, err error) {
|
|||
Column: item.FieldArchived,
|
||||
})
|
||||
}
|
||||
if value, ok := iu.mutation.AssetID(); ok {
|
||||
_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
|
||||
Type: field.TypeInt,
|
||||
Value: value,
|
||||
Column: item.FieldAssetID,
|
||||
})
|
||||
}
|
||||
if value, ok := iu.mutation.AddedAssetID(); ok {
|
||||
_spec.Fields.Add = append(_spec.Fields.Add, &sqlgraph.FieldSpec{
|
||||
Type: field.TypeInt,
|
||||
Value: value,
|
||||
Column: item.FieldAssetID,
|
||||
})
|
||||
}
|
||||
if value, ok := iu.mutation.SerialNumber(); ok {
|
||||
_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
|
||||
Type: field.TypeString,
|
||||
|
@ -1422,6 +1457,27 @@ func (iuo *ItemUpdateOne) SetNillableArchived(b *bool) *ItemUpdateOne {
|
|||
return iuo
|
||||
}
|
||||
|
||||
// SetAssetID sets the "asset_id" field.
|
||||
func (iuo *ItemUpdateOne) SetAssetID(i int) *ItemUpdateOne {
|
||||
iuo.mutation.ResetAssetID()
|
||||
iuo.mutation.SetAssetID(i)
|
||||
return iuo
|
||||
}
|
||||
|
||||
// SetNillableAssetID sets the "asset_id" field if the given value is not nil.
|
||||
func (iuo *ItemUpdateOne) SetNillableAssetID(i *int) *ItemUpdateOne {
|
||||
if i != nil {
|
||||
iuo.SetAssetID(*i)
|
||||
}
|
||||
return iuo
|
||||
}
|
||||
|
||||
// AddAssetID adds i to the "asset_id" field.
|
||||
func (iuo *ItemUpdateOne) AddAssetID(i int) *ItemUpdateOne {
|
||||
iuo.mutation.AddAssetID(i)
|
||||
return iuo
|
||||
}
|
||||
|
||||
// SetSerialNumber sets the "serial_number" field.
|
||||
func (iuo *ItemUpdateOne) SetSerialNumber(s string) *ItemUpdateOne {
|
||||
iuo.mutation.SetSerialNumber(s)
|
||||
|
@ -2133,6 +2189,20 @@ func (iuo *ItemUpdateOne) sqlSave(ctx context.Context) (_node *Item, err error)
|
|||
Column: item.FieldArchived,
|
||||
})
|
||||
}
|
||||
if value, ok := iuo.mutation.AssetID(); ok {
|
||||
_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
|
||||
Type: field.TypeInt,
|
||||
Value: value,
|
||||
Column: item.FieldAssetID,
|
||||
})
|
||||
}
|
||||
if value, ok := iuo.mutation.AddedAssetID(); ok {
|
||||
_spec.Fields.Add = append(_spec.Fields.Add, &sqlgraph.FieldSpec{
|
||||
Type: field.TypeInt,
|
||||
Value: value,
|
||||
Column: item.FieldAssetID,
|
||||
})
|
||||
}
|
||||
if value, ok := iuo.mutation.SerialNumber(); ok {
|
||||
_spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
|
||||
Type: field.TypeString,
|
||||
|
|
|
@ -171,6 +171,7 @@ var (
|
|||
{Name: "quantity", Type: field.TypeInt, Default: 1},
|
||||
{Name: "insured", Type: field.TypeBool, Default: false},
|
||||
{Name: "archived", Type: field.TypeBool, Default: false},
|
||||
{Name: "asset_id", Type: field.TypeInt, Default: 0},
|
||||
{Name: "serial_number", Type: field.TypeString, Nullable: true, Size: 255},
|
||||
{Name: "model_number", Type: field.TypeString, Nullable: true, Size: 255},
|
||||
{Name: "manufacturer", Type: field.TypeString, Nullable: true, Size: 255},
|
||||
|
@ -196,19 +197,19 @@ var (
|
|||
ForeignKeys: []*schema.ForeignKey{
|
||||
{
|
||||
Symbol: "items_groups_items",
|
||||
Columns: []*schema.Column{ItemsColumns[23]},
|
||||
Columns: []*schema.Column{ItemsColumns[24]},
|
||||
RefColumns: []*schema.Column{GroupsColumns[0]},
|
||||
OnDelete: schema.Cascade,
|
||||
},
|
||||
{
|
||||
Symbol: "items_items_children",
|
||||
Columns: []*schema.Column{ItemsColumns[24]},
|
||||
Columns: []*schema.Column{ItemsColumns[25]},
|
||||
RefColumns: []*schema.Column{ItemsColumns[0]},
|
||||
OnDelete: schema.SetNull,
|
||||
},
|
||||
{
|
||||
Symbol: "items_locations_items",
|
||||
Columns: []*schema.Column{ItemsColumns[25]},
|
||||
Columns: []*schema.Column{ItemsColumns[26]},
|
||||
RefColumns: []*schema.Column{LocationsColumns[0]},
|
||||
OnDelete: schema.Cascade,
|
||||
},
|
||||
|
@ -222,23 +223,28 @@ var (
|
|||
{
|
||||
Name: "item_manufacturer",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{ItemsColumns[12]},
|
||||
Columns: []*schema.Column{ItemsColumns[13]},
|
||||
},
|
||||
{
|
||||
Name: "item_model_number",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{ItemsColumns[11]},
|
||||
Columns: []*schema.Column{ItemsColumns[12]},
|
||||
},
|
||||
{
|
||||
Name: "item_serial_number",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{ItemsColumns[10]},
|
||||
Columns: []*schema.Column{ItemsColumns[11]},
|
||||
},
|
||||
{
|
||||
Name: "item_archived",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{ItemsColumns[9]},
|
||||
},
|
||||
{
|
||||
Name: "item_asset_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{ItemsColumns[10]},
|
||||
},
|
||||
},
|
||||
}
|
||||
// ItemFieldsColumns holds the columns for the "item_fields" table.
|
||||
|
|
|
@ -4134,6 +4134,8 @@ type ItemMutation struct {
|
|||
addquantity *int
|
||||
insured *bool
|
||||
archived *bool
|
||||
asset_id *int
|
||||
addasset_id *int
|
||||
serial_number *string
|
||||
model_number *string
|
||||
manufacturer *string
|
||||
|
@ -4660,6 +4662,62 @@ func (m *ItemMutation) ResetArchived() {
|
|||
m.archived = nil
|
||||
}
|
||||
|
||||
// SetAssetID sets the "asset_id" field.
|
||||
func (m *ItemMutation) SetAssetID(i int) {
|
||||
m.asset_id = &i
|
||||
m.addasset_id = nil
|
||||
}
|
||||
|
||||
// AssetID returns the value of the "asset_id" field in the mutation.
|
||||
func (m *ItemMutation) AssetID() (r int, exists bool) {
|
||||
v := m.asset_id
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldAssetID returns the old "asset_id" field's value of the Item entity.
|
||||
// If the Item object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *ItemMutation) OldAssetID(ctx context.Context) (v int, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldAssetID is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldAssetID requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldAssetID: %w", err)
|
||||
}
|
||||
return oldValue.AssetID, nil
|
||||
}
|
||||
|
||||
// AddAssetID adds i to the "asset_id" field.
|
||||
func (m *ItemMutation) AddAssetID(i int) {
|
||||
if m.addasset_id != nil {
|
||||
*m.addasset_id += i
|
||||
} else {
|
||||
m.addasset_id = &i
|
||||
}
|
||||
}
|
||||
|
||||
// AddedAssetID returns the value that was added to the "asset_id" field in this mutation.
|
||||
func (m *ItemMutation) AddedAssetID() (r int, exists bool) {
|
||||
v := m.addasset_id
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// ResetAssetID resets all changes to the "asset_id" field.
|
||||
func (m *ItemMutation) ResetAssetID() {
|
||||
m.asset_id = nil
|
||||
m.addasset_id = nil
|
||||
}
|
||||
|
||||
// SetSerialNumber sets the "serial_number" field.
|
||||
func (m *ItemMutation) SetSerialNumber(s string) {
|
||||
m.serial_number = &s
|
||||
|
@ -5650,7 +5708,7 @@ func (m *ItemMutation) Type() string {
|
|||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *ItemMutation) Fields() []string {
|
||||
fields := make([]string, 0, 22)
|
||||
fields := make([]string, 0, 23)
|
||||
if m.created_at != nil {
|
||||
fields = append(fields, item.FieldCreatedAt)
|
||||
}
|
||||
|
@ -5678,6 +5736,9 @@ func (m *ItemMutation) Fields() []string {
|
|||
if m.archived != nil {
|
||||
fields = append(fields, item.FieldArchived)
|
||||
}
|
||||
if m.asset_id != nil {
|
||||
fields = append(fields, item.FieldAssetID)
|
||||
}
|
||||
if m.serial_number != nil {
|
||||
fields = append(fields, item.FieldSerialNumber)
|
||||
}
|
||||
|
@ -5743,6 +5804,8 @@ func (m *ItemMutation) Field(name string) (ent.Value, bool) {
|
|||
return m.Insured()
|
||||
case item.FieldArchived:
|
||||
return m.Archived()
|
||||
case item.FieldAssetID:
|
||||
return m.AssetID()
|
||||
case item.FieldSerialNumber:
|
||||
return m.SerialNumber()
|
||||
case item.FieldModelNumber:
|
||||
|
@ -5796,6 +5859,8 @@ func (m *ItemMutation) OldField(ctx context.Context, name string) (ent.Value, er
|
|||
return m.OldInsured(ctx)
|
||||
case item.FieldArchived:
|
||||
return m.OldArchived(ctx)
|
||||
case item.FieldAssetID:
|
||||
return m.OldAssetID(ctx)
|
||||
case item.FieldSerialNumber:
|
||||
return m.OldSerialNumber(ctx)
|
||||
case item.FieldModelNumber:
|
||||
|
@ -5894,6 +5959,13 @@ func (m *ItemMutation) SetField(name string, value ent.Value) error {
|
|||
}
|
||||
m.SetArchived(v)
|
||||
return nil
|
||||
case item.FieldAssetID:
|
||||
v, ok := value.(int)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetAssetID(v)
|
||||
return nil
|
||||
case item.FieldSerialNumber:
|
||||
v, ok := value.(string)
|
||||
if !ok {
|
||||
|
@ -5996,6 +6068,9 @@ func (m *ItemMutation) AddedFields() []string {
|
|||
if m.addquantity != nil {
|
||||
fields = append(fields, item.FieldQuantity)
|
||||
}
|
||||
if m.addasset_id != nil {
|
||||
fields = append(fields, item.FieldAssetID)
|
||||
}
|
||||
if m.addpurchase_price != nil {
|
||||
fields = append(fields, item.FieldPurchasePrice)
|
||||
}
|
||||
|
@ -6012,6 +6087,8 @@ func (m *ItemMutation) AddedField(name string) (ent.Value, bool) {
|
|||
switch name {
|
||||
case item.FieldQuantity:
|
||||
return m.AddedQuantity()
|
||||
case item.FieldAssetID:
|
||||
return m.AddedAssetID()
|
||||
case item.FieldPurchasePrice:
|
||||
return m.AddedPurchasePrice()
|
||||
case item.FieldSoldPrice:
|
||||
|
@ -6032,6 +6109,13 @@ func (m *ItemMutation) AddField(name string, value ent.Value) error {
|
|||
}
|
||||
m.AddQuantity(v)
|
||||
return nil
|
||||
case item.FieldAssetID:
|
||||
v, ok := value.(int)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.AddAssetID(v)
|
||||
return nil
|
||||
case item.FieldPurchasePrice:
|
||||
v, ok := value.(float64)
|
||||
if !ok {
|
||||
|
@ -6181,6 +6265,9 @@ func (m *ItemMutation) ResetField(name string) error {
|
|||
case item.FieldArchived:
|
||||
m.ResetArchived()
|
||||
return nil
|
||||
case item.FieldAssetID:
|
||||
m.ResetAssetID()
|
||||
return nil
|
||||
case item.FieldSerialNumber:
|
||||
m.ResetSerialNumber()
|
||||
return nil
|
||||
|
|
|
@ -275,36 +275,40 @@ func init() {
|
|||
itemDescArchived := itemFields[4].Descriptor()
|
||||
// item.DefaultArchived holds the default value on creation for the archived field.
|
||||
item.DefaultArchived = itemDescArchived.Default.(bool)
|
||||
// itemDescAssetID is the schema descriptor for asset_id field.
|
||||
itemDescAssetID := itemFields[5].Descriptor()
|
||||
// item.DefaultAssetID holds the default value on creation for the asset_id field.
|
||||
item.DefaultAssetID = itemDescAssetID.Default.(int)
|
||||
// itemDescSerialNumber is the schema descriptor for serial_number field.
|
||||
itemDescSerialNumber := itemFields[5].Descriptor()
|
||||
itemDescSerialNumber := itemFields[6].Descriptor()
|
||||
// item.SerialNumberValidator is a validator for the "serial_number" field. It is called by the builders before save.
|
||||
item.SerialNumberValidator = itemDescSerialNumber.Validators[0].(func(string) error)
|
||||
// itemDescModelNumber is the schema descriptor for model_number field.
|
||||
itemDescModelNumber := itemFields[6].Descriptor()
|
||||
itemDescModelNumber := itemFields[7].Descriptor()
|
||||
// item.ModelNumberValidator is a validator for the "model_number" field. It is called by the builders before save.
|
||||
item.ModelNumberValidator = itemDescModelNumber.Validators[0].(func(string) error)
|
||||
// itemDescManufacturer is the schema descriptor for manufacturer field.
|
||||
itemDescManufacturer := itemFields[7].Descriptor()
|
||||
itemDescManufacturer := itemFields[8].Descriptor()
|
||||
// item.ManufacturerValidator is a validator for the "manufacturer" field. It is called by the builders before save.
|
||||
item.ManufacturerValidator = itemDescManufacturer.Validators[0].(func(string) error)
|
||||
// itemDescLifetimeWarranty is the schema descriptor for lifetime_warranty field.
|
||||
itemDescLifetimeWarranty := itemFields[8].Descriptor()
|
||||
itemDescLifetimeWarranty := itemFields[9].Descriptor()
|
||||
// item.DefaultLifetimeWarranty holds the default value on creation for the lifetime_warranty field.
|
||||
item.DefaultLifetimeWarranty = itemDescLifetimeWarranty.Default.(bool)
|
||||
// itemDescWarrantyDetails is the schema descriptor for warranty_details field.
|
||||
itemDescWarrantyDetails := itemFields[10].Descriptor()
|
||||
itemDescWarrantyDetails := itemFields[11].Descriptor()
|
||||
// item.WarrantyDetailsValidator is a validator for the "warranty_details" field. It is called by the builders before save.
|
||||
item.WarrantyDetailsValidator = itemDescWarrantyDetails.Validators[0].(func(string) error)
|
||||
// itemDescPurchasePrice is the schema descriptor for purchase_price field.
|
||||
itemDescPurchasePrice := itemFields[13].Descriptor()
|
||||
itemDescPurchasePrice := itemFields[14].Descriptor()
|
||||
// item.DefaultPurchasePrice holds the default value on creation for the purchase_price field.
|
||||
item.DefaultPurchasePrice = itemDescPurchasePrice.Default.(float64)
|
||||
// itemDescSoldPrice is the schema descriptor for sold_price field.
|
||||
itemDescSoldPrice := itemFields[16].Descriptor()
|
||||
itemDescSoldPrice := itemFields[17].Descriptor()
|
||||
// item.DefaultSoldPrice holds the default value on creation for the sold_price field.
|
||||
item.DefaultSoldPrice = itemDescSoldPrice.Default.(float64)
|
||||
// itemDescSoldNotes is the schema descriptor for sold_notes field.
|
||||
itemDescSoldNotes := itemFields[17].Descriptor()
|
||||
itemDescSoldNotes := itemFields[18].Descriptor()
|
||||
// item.SoldNotesValidator is a validator for the "sold_notes" field. It is called by the builders before save.
|
||||
item.SoldNotesValidator = itemDescSoldNotes.Validators[0].(func(string) error)
|
||||
// itemDescID is the schema descriptor for id field.
|
||||
|
|
|
@ -29,6 +29,7 @@ func (Item) Indexes() []ent.Index {
|
|||
index.Fields("model_number"),
|
||||
index.Fields("serial_number"),
|
||||
index.Fields("archived"),
|
||||
index.Fields("asset_id"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,6 +49,8 @@ func (Item) Fields() []ent.Field {
|
|||
Default(false),
|
||||
field.Bool("archived").
|
||||
Default(false),
|
||||
field.Int("asset_id").
|
||||
Default(0),
|
||||
|
||||
// ------------------------------------
|
||||
// item identification
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
-- 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, `archived` bool NOT NULL DEFAULT false, `asset_id` integer NOT NULL DEFAULT 0, `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`, `archived`, `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`, `item_children`, `location_items`) SELECT `id`, `created_at`, `updated_at`, `name`, `description`, `import_ref`, `notes`, `quantity`, `insured`, `archived`, `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`, `item_children`, `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 index "item_archived" to table: "items"
|
||||
CREATE INDEX `item_archived` ON `items` (`archived`);
|
||||
-- create index "item_asset_id" to table: "items"
|
||||
CREATE INDEX `item_asset_id` ON `items` (`asset_id`);
|
||||
-- enable back the enforcement of foreign-keys constraints
|
||||
PRAGMA foreign_keys = on;
|
|
@ -1,6 +1,7 @@
|
|||
h1:i76VRMDIPdcmQtXTe9bzrgITAzLGjjVy9y8XaXIchAs=
|
||||
h1:z1tbZ3fYByqxL78Z+ov8mfQVjXcwsZeEcT0i+2DZ8a8=
|
||||
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=
|
||||
20221101041931_add_archived_field.sql h1:L2WxiOh1svRn817cNURgqnEQg6DIcodZ1twK4tvxW94=
|
||||
20221113012312_add_asset_id_field.sql h1:DjD7e1PS8OfxGBWic8h0nO/X6CNnHEMqQjDCaaQ3M3Q=
|
||||
|
|
30
backend/internal/data/repo/asset_id_type.go
Normal file
30
backend/internal/data/repo/asset_id_type.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type AssetID int
|
||||
|
||||
func (aid AssetID) MarshalJSON() ([]byte, error) {
|
||||
aidStr := fmt.Sprintf("%06d", aid)
|
||||
aidStr = fmt.Sprintf("%s-%s", aidStr[:3], aidStr[3:])
|
||||
return []byte(fmt.Sprintf(`"%s"`, aidStr)), nil
|
||||
|
||||
}
|
||||
|
||||
func (aid *AssetID) UnmarshalJSON(d []byte) error {
|
||||
d = bytes.Replace(d, []byte(`"`), []byte(``), -1)
|
||||
d = bytes.Replace(d, []byte(`-`), []byte(``), -1)
|
||||
|
||||
aidInt, err := strconv.Atoi(string(d))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*aid = AssetID(aidInt)
|
||||
return nil
|
||||
|
||||
}
|
115
backend/internal/data/repo/asset_id_type_test.go
Normal file
115
backend/internal/data/repo/asset_id_type_test.go
Normal file
|
@ -0,0 +1,115 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAssetID_MarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
aid AssetID
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "basic test",
|
||||
aid: 123,
|
||||
want: []byte(`"000-123"`),
|
||||
},
|
||||
{
|
||||
name: "zero test",
|
||||
aid: 0,
|
||||
want: []byte(`"000-000"`),
|
||||
},
|
||||
{
|
||||
name: "large int",
|
||||
aid: 123456789,
|
||||
want: []byte(`"123-456789"`),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.aid.MarshalJSON()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("AssetID.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("AssetID.MarshalJSON() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssetID_UnmarshalJSON(t *testing.T) {
|
||||
type args struct {
|
||||
data []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
aid *AssetID
|
||||
args args
|
||||
want AssetID
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "basic test",
|
||||
aid: new(AssetID),
|
||||
want: 123,
|
||||
args: args{
|
||||
data: []byte(`{"AssetID":"000123"}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dashed format",
|
||||
aid: new(AssetID),
|
||||
want: 123,
|
||||
args: args{
|
||||
data: []byte(`{"AssetID":"000-123"}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no leading zeros",
|
||||
aid: new(AssetID),
|
||||
want: 123,
|
||||
args: args{
|
||||
data: []byte(`{"AssetID":"123"}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "trailing zeros",
|
||||
aid: new(AssetID),
|
||||
want: 123000,
|
||||
args: args{
|
||||
data: []byte(`{"AssetID":"000123000"}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "large int",
|
||||
aid: new(AssetID),
|
||||
want: 123456789,
|
||||
args: args{
|
||||
data: []byte(`{"AssetID":"123456789"}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
st := struct {
|
||||
AssetID AssetID `json:"AssetID"`
|
||||
}{}
|
||||
|
||||
err := json.Unmarshal(tt.args.data, &st)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("AssetID.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if st.AssetID != tt.want {
|
||||
t.Errorf("AssetID.UnmarshalJSON() = %v, want %v", st.AssetID, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -44,6 +44,7 @@ type (
|
|||
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
AssetID AssetID `json:"-"`
|
||||
|
||||
// Edges
|
||||
LocationID uuid.UUID `json:"locationId"`
|
||||
|
@ -52,6 +53,7 @@ type (
|
|||
ItemUpdate struct {
|
||||
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable,x-omitempty"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
AssetID AssetID `json:"assetId"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Quantity int `json:"quantity"`
|
||||
|
@ -107,6 +109,7 @@ type (
|
|||
ItemOut struct {
|
||||
Parent *ItemSummary `json:"parent,omitempty" extensions:"x-nullable,x-omitempty"`
|
||||
ItemSummary
|
||||
AssetID AssetID `json:"assetId,string"`
|
||||
|
||||
SerialNumber string `json:"serialNumber"`
|
||||
ModelNumber string `json:"modelNumber"`
|
||||
|
@ -215,6 +218,7 @@ func mapItemOut(item *ent.Item) ItemOut {
|
|||
|
||||
return ItemOut{
|
||||
Parent: parent,
|
||||
AssetID: AssetID(item.AssetID),
|
||||
ItemSummary: mapItemSummary(item),
|
||||
LifetimeWarranty: item.LifetimeWarranty,
|
||||
WarrantyExpires: item.WarrantyExpires,
|
||||
|
@ -359,13 +363,53 @@ func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]ItemSumm
|
|||
All(ctx))
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) GetAllZeroAssetID(ctx context.Context, GID uuid.UUID) ([]ItemSummary, error) {
|
||||
q := e.db.Item.Query().Where(
|
||||
item.HasGroupWith(group.ID(GID)),
|
||||
item.AssetID(0),
|
||||
).Order(
|
||||
ent.Asc(item.FieldCreatedAt),
|
||||
)
|
||||
|
||||
return mapItemsSummaryErr(q.All(ctx))
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) GetHighestAssetID(ctx context.Context, GID uuid.UUID) (AssetID, error) {
|
||||
q := e.db.Item.Query().Where(
|
||||
item.HasGroupWith(group.ID(GID)),
|
||||
).Order(
|
||||
ent.Desc(item.FieldAssetID),
|
||||
).Limit(1)
|
||||
|
||||
result, err := q.First(ctx)
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return AssetID(result.AssetID), nil
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) SetAssetID(ctx context.Context, GID uuid.UUID, ID uuid.UUID, assetID AssetID) error {
|
||||
q := e.db.Item.Update().Where(
|
||||
item.HasGroupWith(group.ID(GID)),
|
||||
item.ID(ID),
|
||||
)
|
||||
|
||||
_, err := q.SetAssetID(int(assetID)).Save(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) Create(ctx context.Context, gid uuid.UUID, data ItemCreate) (ItemOut, error) {
|
||||
q := e.db.Item.Create().
|
||||
SetImportRef(data.ImportRef).
|
||||
SetName(data.Name).
|
||||
SetDescription(data.Description).
|
||||
SetGroupID(gid).
|
||||
SetLocationID(data.LocationID)
|
||||
SetLocationID(data.LocationID).
|
||||
SetAssetID(int(data.AssetID))
|
||||
|
||||
if data.LabelIDs != nil && len(data.LabelIDs) > 0 {
|
||||
q.AddLabelIDs(data.LabelIDs...)
|
||||
|
@ -414,7 +458,8 @@ func (e *ItemsRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data
|
|||
SetInsured(data.Insured).
|
||||
SetWarrantyExpires(data.WarrantyExpires).
|
||||
SetWarrantyDetails(data.WarrantyDetails).
|
||||
SetQuantity(data.Quantity)
|
||||
SetQuantity(data.Quantity).
|
||||
SetAssetID(int(data.AssetID))
|
||||
|
||||
currentLabels, err := e.db.Item.Query().Where(item.ID(data.ID)).QueryLabel().All(ctx)
|
||||
if err != nil {
|
||||
|
|
|
@ -23,8 +23,13 @@ type Config struct {
|
|||
Mailer MailerConf `yaml:"mailer"`
|
||||
Swagger SwaggerConf `yaml:"swagger"`
|
||||
Demo bool `yaml:"demo"`
|
||||
AllowRegistration bool `yaml:"disable_registration" conf:"default:true"`
|
||||
Debug DebugConf `yaml:"debug"`
|
||||
Options Options `yaml:"options"`
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
AllowRegistration bool `yaml:"disable_registration" conf:"default:true"`
|
||||
AutoIncrementAssetID bool `yaml:"auto_increment_asset_id" conf:"default:true"`
|
||||
}
|
||||
|
||||
type DebugConf struct {
|
||||
|
|
|
@ -38,11 +38,12 @@ volumes:
|
|||
## Env Variables & Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------ | ---------------------- | ---------------------------------------------------------------------------------- |
|
||||
| ------------------------------------ | ---------------------- | ---------------------------------------------------------------------------------- |
|
||||
| HBOX_MODE | production | application mode used for runtime behavior can be one of: development, production |
|
||||
| HBOX_WEB_PORT | 7745 | port to run the web server on, if you're using docker do not change this |
|
||||
| HBOX_WEB_HOST | | host to run the web server on, if you're using docker do not change this |
|
||||
| HBOX_ALLOW_REGISTRATION | true | allow users to register themselves |
|
||||
| HBOX_OPTIONS_ALLOW_REGISTRATION | true | allow users to register themselves |
|
||||
| HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID | true | auto increments the asset_id field for new items |
|
||||
| HBOX_WEB_MAX_UPLOAD_SIZE | 10 | maximum file upload size supported in MB |
|
||||
| HBOX_STORAGE_DATA | /data/ | path to the data directory, do not change this if you're using docker |
|
||||
| HBOX_STORAGE_SQLITE_URL | /data/homebox.db?_fk=1 | sqlite database url, in you're using docker do not change this |
|
||||
|
@ -79,7 +80,10 @@ volumes:
|
|||
--swagger-host/$HBOX_SWAGGER_HOST <string> (default: localhost:7745)
|
||||
--swagger-scheme/$HBOX_SWAGGER_SCHEME <string> (default: http)
|
||||
--demo/$HBOX_DEMO <bool>
|
||||
--allow-registration/$HBOX_ALLOW_REGISTRATION <bool> (default: true)
|
||||
--debug-enabled/$HBOX_DEBUG_ENABLED <bool> (default: false)
|
||||
--debug-port/$HBOX_DEBUG_PORT <string> (default: 4000)
|
||||
--options-allow-registration/$HBOX_OPTIONS_ALLOW_REGISTRATION <bool> (default: true)
|
||||
--options-auto-increment-asset-id/$HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID <bool> (default: true)
|
||||
--help/-h
|
||||
display this help message
|
||||
```
|
||||
|
|
|
@ -14,3 +14,21 @@ Custom fields are appended to the main details section of your item.
|
|||
!!! tip
|
||||
Homebox Custom Fields also have special support for URLs. Provide a URL (`https://google.com`) and it will be automatically converted to a clickable link in the UI. Optionally, you can also use markdown syntax to add a custom text to the button. `[Google](https://google.com)`
|
||||
|
||||
## Managing Asset IDs
|
||||
|
||||
Homebox provides the option to auto-set asset IDs, this is the default behavior. These can be used for tracking assets with printable tags or labels. You can disable this behavior via a command line flag or ENV variable. See [configuration](../quick-start#env-variables-configuration) for more details.
|
||||
|
||||
Example ID: `000-001`
|
||||
|
||||
Asset IDs are partially managed by Homebox, but have a flexible implementation to allow for unique use cases. ID's are non-unique at the database level so there is nothing stopping a user from manually setting duplicate IDs for various items. There are two recommended approaches to manage Asset IDs
|
||||
|
||||
### 1. Auto Incrementing IDs
|
||||
|
||||
This is the default behavior and likely to one to experience the most consistent behavior. Whenever creating or importing an item, that items receives the next available ID. This is the most consistent approach and is recommended for most users.
|
||||
|
||||
### 2. Auto Incrementing ID's with Reset
|
||||
|
||||
In some cases you may want to skip some items such as consumables, or items that are loosely tracked. In this case, we recommend that you leave auto-incrementing ID's enabled _however_ when you create a new item that you want to skip, you can go to that item and reset the ID to 0. This will remove it from the auto-incrementing sequence and the next item will receive the next available ID.
|
||||
|
||||
!!! tip
|
||||
If you're migrating from an older version there is a action on the users profile page to assign IDs to all items. This will assign the next available ID to all items in the order of creation. You should _only_ do this once during the migration process. You should be especially cautious of this action if you're using the reset feature described in option number 2
|
||||
|
|
10
frontend/lib/api/classes/actions.ts
Normal file
10
frontend/lib/api/classes/actions.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { BaseAPI, route } from "../base";
|
||||
import { EnsureAssetIDResult } from "../types/data-contracts";
|
||||
|
||||
export class ActionsAPI extends BaseAPI {
|
||||
ensureAssetIDs() {
|
||||
return this.http.post<void, EnsureAssetIDResult>({
|
||||
url: route("/actions/ensure-asset-ids"),
|
||||
});
|
||||
}
|
||||
}
|
|
@ -71,6 +71,9 @@ export interface ItemField {
|
|||
|
||||
export interface ItemOut {
|
||||
archived: boolean;
|
||||
|
||||
/** @example 0 */
|
||||
assetId: string;
|
||||
attachments: ItemAttachment[];
|
||||
children: ItemSummary[];
|
||||
createdAt: Date;
|
||||
|
@ -131,6 +134,7 @@ export interface ItemSummary {
|
|||
|
||||
export interface ItemUpdate {
|
||||
archived: boolean;
|
||||
assetId: string;
|
||||
description: string;
|
||||
fields: ItemField[];
|
||||
id: string;
|
||||
|
@ -300,6 +304,10 @@ export interface ChangePassword {
|
|||
new: string;
|
||||
}
|
||||
|
||||
export interface EnsureAssetIDResult {
|
||||
completed: number;
|
||||
}
|
||||
|
||||
export interface GroupInvitation {
|
||||
expiresAt: Date;
|
||||
token: string;
|
||||
|
|
|
@ -4,6 +4,7 @@ import { LabelsApi } from "./classes/labels";
|
|||
import { LocationsApi } from "./classes/locations";
|
||||
import { GroupApi } from "./classes/group";
|
||||
import { UserApi } from "./classes/users";
|
||||
import { ActionsAPI } from "./classes/actions";
|
||||
import { Requests } from "~~/lib/requests";
|
||||
|
||||
export class UserClient extends BaseAPI {
|
||||
|
@ -12,6 +13,7 @@ export class UserClient extends BaseAPI {
|
|||
items: ItemsApi;
|
||||
group: GroupApi;
|
||||
user: UserApi;
|
||||
actions: ActionsAPI;
|
||||
|
||||
constructor(requests: Requests) {
|
||||
super(requests);
|
||||
|
@ -21,6 +23,7 @@ export class UserClient extends BaseAPI {
|
|||
this.items = new ItemsApi(requests);
|
||||
this.group = new GroupApi(requests);
|
||||
this.user = new UserApi(requests);
|
||||
this.actions = new ActionsAPI(requests);
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
|
|
@ -120,6 +120,11 @@
|
|||
label: "Archived",
|
||||
ref: "archived",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
label: "Asset ID",
|
||||
ref: "assetId",
|
||||
},
|
||||
];
|
||||
|
||||
const purchaseFields: FormField[] = [
|
||||
|
|
|
@ -70,6 +70,19 @@
|
|||
);
|
||||
});
|
||||
|
||||
const assetID = computed<Details>(() => {
|
||||
if (item.value?.assetId === "000-000") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
name: "Asset ID",
|
||||
text: item.value?.assetId,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const itemDetails = computed<Details>(() => {
|
||||
return [
|
||||
{
|
||||
|
@ -100,6 +113,7 @@
|
|||
name: "Notes",
|
||||
text: item.value?.notes,
|
||||
},
|
||||
...assetID.value,
|
||||
...item.value.fields.map(field => {
|
||||
/**
|
||||
* Support Special URL Syntax
|
||||
|
|
|
@ -163,6 +163,25 @@
|
|||
passwordChange.current = "";
|
||||
passwordChange.loading = false;
|
||||
}
|
||||
|
||||
async function ensureAssetIDs() {
|
||||
const { isCanceled } = await confirm.open(
|
||||
"Are you sure you want to ensure all assets have an ID? This will take a while and cannot be undone."
|
||||
);
|
||||
|
||||
if (isCanceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api.actions.ensureAssetIDs();
|
||||
|
||||
if (result.error) {
|
||||
notify.error("Failed to ensure asset IDs.");
|
||||
return;
|
||||
}
|
||||
|
||||
notify.success(`${result.data.completed} assets have been updated.`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -286,6 +305,32 @@
|
|||
</div>
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard>
|
||||
<template #title>
|
||||
<BaseSectionHeader>
|
||||
<Icon name="mdi-warning" class="mr-2 -mt-1 text-base-600" />
|
||||
<span class="text-base-600"> Actions </span>
|
||||
<template #description>
|
||||
Apply Actions to your inventory in bulk. These are irreversible actions. Be careful.
|
||||
</template>
|
||||
</BaseSectionHeader>
|
||||
|
||||
<div class="py-4 border-t-2 border-gray-300">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-10">
|
||||
<div class="col-span-3">
|
||||
<h4>Manage Asset IDs</h4>
|
||||
<p class="text-sm">
|
||||
Ensures that all items in your inventory have a valid asset_id field. This is done by finding the
|
||||
highest current asset_id field in the database and applying the next value to each item that has an
|
||||
unset asset_id field. This is done in order of the created_at field.
|
||||
</p>
|
||||
</div>
|
||||
<BaseButton class="btn-primary mt-auto" @click="ensureAssetIDs"> Ensure Asset IDs </BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard>
|
||||
<template #title>
|
||||
<BaseSectionHeader>
|
||||
|
|
Loading…
Reference in a new issue