bulk seed asset IDs

This commit is contained in:
Hayden 2022-11-12 18:57:51 -09:00
parent ab406baf33
commit 567e12a1e9
No known key found for this signature in database
GPG key ID: 17CF79474E257545
13 changed files with 331 additions and 1 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

@ -82,6 +82,8 @@ func (a *app) mountRoutes(repos *repo.AllRepos) {
a.server.Get(v1Base("/groups"), v1Ctrl.HandleGroupGet(), a.mwAuthToken) a.server.Get(v1Base("/groups"), v1Ctrl.HandleGroupGet(), a.mwAuthToken)
a.server.Put(v1Base("/groups"), v1Ctrl.HandleGroupUpdate(), 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.Get(v1Base("/locations"), v1Ctrl.HandleLocationGetAll(), a.mwAuthToken)
a.server.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate(), a.mwAuthToken) a.server.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate(), a.mwAuthToken)
a.server.Get(v1Base("/locations/{id}"), v1Ctrl.HandleLocationGet(), a.mwAuthToken) a.server.Get(v1Base("/locations/{id}"), v1Ctrl.HandleLocationGet(), a.mwAuthToken)

View file

@ -21,6 +21,30 @@ const docTemplate = `{
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": { "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": { "/v1/groups": {
"get": { "get": {
"security": [ "security": [
@ -1326,6 +1350,10 @@ const docTemplate = `{
"archived": { "archived": {
"type": "boolean" "type": "boolean"
}, },
"assetId": {
"type": "string",
"example": "0"
},
"attachments": { "attachments": {
"type": "array", "type": "array",
"items": { "items": {
@ -1479,6 +1507,9 @@ const docTemplate = `{
"archived": { "archived": {
"type": "boolean" "type": "boolean"
}, },
"assetId": {
"type": "string"
},
"description": { "description": {
"type": "string" "type": "string"
}, },
@ -1891,6 +1922,14 @@ const docTemplate = `{
} }
} }
}, },
"v1.EnsureAssetIDResult": {
"type": "object",
"properties": {
"completed": {
"type": "integer"
}
}
},
"v1.GroupInvitation": { "v1.GroupInvitation": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -13,6 +13,30 @@
}, },
"basePath": "/api", "basePath": "/api",
"paths": { "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": { "/v1/groups": {
"get": { "get": {
"security": [ "security": [
@ -1318,6 +1342,10 @@
"archived": { "archived": {
"type": "boolean" "type": "boolean"
}, },
"assetId": {
"type": "string",
"example": "0"
},
"attachments": { "attachments": {
"type": "array", "type": "array",
"items": { "items": {
@ -1471,6 +1499,9 @@
"archived": { "archived": {
"type": "boolean" "type": "boolean"
}, },
"assetId": {
"type": "string"
},
"description": { "description": {
"type": "string" "type": "string"
}, },
@ -1883,6 +1914,14 @@
} }
} }
}, },
"v1.EnsureAssetIDResult": {
"type": "object",
"properties": {
"completed": {
"type": "integer"
}
}
},
"v1.GroupInvitation": { "v1.GroupInvitation": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -98,6 +98,9 @@ definitions:
properties: properties:
archived: archived:
type: boolean type: boolean
assetId:
example: "0"
type: string
attachments: attachments:
items: items:
$ref: '#/definitions/repo.ItemAttachment' $ref: '#/definitions/repo.ItemAttachment'
@ -204,6 +207,8 @@ definitions:
properties: properties:
archived: archived:
type: boolean type: boolean
assetId:
type: string
description: description:
type: string type: string
fields: fields:
@ -477,6 +482,11 @@ definitions:
new: new:
type: string type: string
type: object type: object
v1.EnsureAssetIDResult:
properties:
completed:
type: integer
type: object
v1.GroupInvitation: v1.GroupInvitation:
properties: properties:
expiresAt: expiresAt:
@ -516,6 +526,20 @@ info:
title: Go API Templates title: Go API Templates
version: "1.0" version: "1.0"
paths: 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: /v1/groups:
get: get:
produces: produces:

View file

@ -23,6 +23,32 @@ type ItemService struct {
at attachmentTokens at attachmentTokens
} }
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) { func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data [][]string) (int, error) {
loaded := []csvRow{} loaded := []csvRow{}

View file

@ -2,6 +2,9 @@ package repo
import ( import (
"context" "context"
"fmt"
"strconv"
"strings"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
@ -18,6 +21,30 @@ type ItemsRepository struct {
db *ent.Client db *ent.Client
} }
type AssetID int
func (aid AssetID) MarshalJSON() ([]byte, error) {
str := fmt.Sprintf("%d", aid)
for len(str) < 6 {
str = "0" + str
}
return []byte(fmt.Sprintf(`"%s"`, str)), nil
}
func (aid *AssetID) UnmarshalJSON(data []byte) error {
str := string(strings.Replace(string(data), `"`, "", -1))
aidInt, err := strconv.Atoi(str)
if err != nil {
return err
}
*aid = AssetID(aidInt)
return nil
}
type ( type (
ItemQuery struct { ItemQuery struct {
Page int Page int
@ -52,6 +79,7 @@ type (
ItemUpdate struct { ItemUpdate struct {
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable,x-omitempty"` ParentID uuid.UUID `json:"parentId" extensions:"x-nullable,x-omitempty"`
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
AssetID AssetID `json:"assetId"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Quantity int `json:"quantity"` Quantity int `json:"quantity"`
@ -107,6 +135,7 @@ type (
ItemOut struct { ItemOut struct {
Parent *ItemSummary `json:"parent,omitempty" extensions:"x-nullable,x-omitempty"` Parent *ItemSummary `json:"parent,omitempty" extensions:"x-nullable,x-omitempty"`
ItemSummary ItemSummary
AssetID AssetID `json:"assetId,string"`
SerialNumber string `json:"serialNumber"` SerialNumber string `json:"serialNumber"`
ModelNumber string `json:"modelNumber"` ModelNumber string `json:"modelNumber"`
@ -215,6 +244,7 @@ func mapItemOut(item *ent.Item) ItemOut {
return ItemOut{ return ItemOut{
Parent: parent, Parent: parent,
AssetID: AssetID(item.AssetID),
ItemSummary: mapItemSummary(item), ItemSummary: mapItemSummary(item),
LifetimeWarranty: item.LifetimeWarranty, LifetimeWarranty: item.LifetimeWarranty,
WarrantyExpires: item.WarrantyExpires, WarrantyExpires: item.WarrantyExpires,
@ -359,6 +389,42 @@ func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]ItemSumm
All(ctx)) 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 {
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) { func (e *ItemsRepository) Create(ctx context.Context, gid uuid.UUID, data ItemCreate) (ItemOut, error) {
q := e.db.Item.Create(). q := e.db.Item.Create().
SetImportRef(data.ImportRef). SetImportRef(data.ImportRef).
@ -414,7 +480,8 @@ func (e *ItemsRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data
SetInsured(data.Insured). SetInsured(data.Insured).
SetWarrantyExpires(data.WarrantyExpires). SetWarrantyExpires(data.WarrantyExpires).
SetWarrantyDetails(data.WarrantyDetails). 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) currentLabels, err := e.db.Item.Query().Where(item.ID(data.ID)).QueryLabel().All(ctx)
if err != nil { if err != nil {

View file

@ -2,6 +2,7 @@ package repo
import ( import (
"context" "context"
"encoding/json"
"testing" "testing"
"time" "time"
@ -9,6 +10,33 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestAssetID_UnmarshalJSON(t *testing.T) {
rawjson := `{"aid":"000123"}`
st := struct {
AID AssetID `json:"aid"`
}{
AID: AssetID(0),
}
err := json.Unmarshal([]byte(rawjson), &st)
assert.NoError(t, err)
assert.Equal(t, AssetID(123), st.AID)
}
func TestAssetID_MarshalJSON(t *testing.T) {
st := struct {
AID AssetID `json:"aid"`
}{
AID: AssetID(123),
}
b, err := json.Marshal(st)
assert.NoError(t, err)
assert.JSONEq(t, `{"aid":"000123"}`, string(b))
}
func itemFactory() ItemCreate { func itemFactory() ItemCreate {
return ItemCreate{ return ItemCreate{
Name: fk.Str(10), Name: fk.Str(10),

View 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"),
});
}
}

View file

@ -71,6 +71,9 @@ export interface ItemField {
export interface ItemOut { export interface ItemOut {
archived: boolean; archived: boolean;
/** @example 0 */
assetId: string;
attachments: ItemAttachment[]; attachments: ItemAttachment[];
children: ItemSummary[]; children: ItemSummary[];
createdAt: Date; createdAt: Date;
@ -131,6 +134,7 @@ export interface ItemSummary {
export interface ItemUpdate { export interface ItemUpdate {
archived: boolean; archived: boolean;
assetId: string;
description: string; description: string;
fields: ItemField[]; fields: ItemField[];
id: string; id: string;
@ -300,6 +304,10 @@ export interface ChangePassword {
new: string; new: string;
} }
export interface EnsureAssetIDResult {
completed: number;
}
export interface GroupInvitation { export interface GroupInvitation {
expiresAt: Date; expiresAt: Date;
token: string; token: string;

View file

@ -4,6 +4,7 @@ import { LabelsApi } from "./classes/labels";
import { LocationsApi } from "./classes/locations"; import { LocationsApi } from "./classes/locations";
import { GroupApi } from "./classes/group"; import { GroupApi } from "./classes/group";
import { UserApi } from "./classes/users"; import { UserApi } from "./classes/users";
import { ActionsAPI } from "./classes/actions";
import { Requests } from "~~/lib/requests"; import { Requests } from "~~/lib/requests";
export class UserClient extends BaseAPI { export class UserClient extends BaseAPI {
@ -12,6 +13,7 @@ export class UserClient extends BaseAPI {
items: ItemsApi; items: ItemsApi;
group: GroupApi; group: GroupApi;
user: UserApi; user: UserApi;
actions: ActionsAPI;
constructor(requests: Requests) { constructor(requests: Requests) {
super(requests); super(requests);
@ -21,6 +23,7 @@ export class UserClient extends BaseAPI {
this.items = new ItemsApi(requests); this.items = new ItemsApi(requests);
this.group = new GroupApi(requests); this.group = new GroupApi(requests);
this.user = new UserApi(requests); this.user = new UserApi(requests);
this.actions = new ActionsAPI(requests);
Object.freeze(this); Object.freeze(this);
} }

View file

@ -100,6 +100,10 @@
name: "Notes", name: "Notes",
text: item.value?.notes, text: item.value?.notes,
}, },
{
name: "Asset ID",
text: item.value?.assetId,
},
...item.value.fields.map(field => { ...item.value.fields.map(field => {
/** /**
* Support Special URL Syntax * Support Special URL Syntax

View file

@ -163,6 +163,25 @@
passwordChange.current = ""; passwordChange.current = "";
passwordChange.loading = false; 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> </script>
<template> <template>
@ -286,6 +305,32 @@
</div> </div>
</BaseCard> </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> <BaseCard>
<template #title> <template #title>
<BaseSectionHeader> <BaseSectionHeader>