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:
Hayden 2022-11-13 14:17:55 -09:00 committed by GitHub
parent 976f68252d
commit 6dc2ae1bea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 905 additions and 72 deletions

View 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})
}
}

View file

@ -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)

View file

@ -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\

View file

@ -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)

View file

@ -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": {

View file

@ -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": {

View file

@ -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:

View file

@ -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{},
repo: repos,
at: attachmentTokens{},
autoIncrementAssetID: options.autoIncrementAssetID,
},
}
}

View file

@ -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,

View file

@ -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(", ")

View file

@ -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.

View file

@ -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) {

View file

@ -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,

View file

@ -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,

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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;

View file

@ -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=

View 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
}

View 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)
}
})
}
}

View file

@ -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 {

View file

@ -16,15 +16,20 @@ const (
)
type Config struct {
Mode string `yaml:"mode" conf:"default:development"` // development or production
Web WebConfig `yaml:"web"`
Storage Storage `yaml:"storage"`
Log LoggerConf `yaml:"logger"`
Mailer MailerConf `yaml:"mailer"`
Swagger SwaggerConf `yaml:"swagger"`
Demo bool `yaml:"demo"`
AllowRegistration bool `yaml:"disable_registration" conf:"default:true"`
Debug DebugConf `yaml:"debug"`
Mode string `yaml:"mode" conf:"default:development"` // development or production
Web WebConfig `yaml:"web"`
Storage Storage `yaml:"storage"`
Log LoggerConf `yaml:"logger"`
Mailer MailerConf `yaml:"mailer"`
Swagger SwaggerConf `yaml:"swagger"`
Demo bool `yaml:"demo"`
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 {