From 0cbe516ca3f493fc9cecb97903f06979ff6e8009 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:00:57 -0800 Subject: [PATCH] feat: WebSocket based implementation of server sent events for cache busting (#527) * rough implementation of WS based event system for server side notifications of mutation * fix test construction * fix deadlock on event bus * disable linter error * add item mutation events * remove old event bus code * refactor event system to use composables * refresh items table when new item is added * fix create form errors * cleanup unnecessary calls * fix importer erorrs + limit fn calls on import Former-commit-id: 2cbcc8bb1d9a4393c28825d718b06b423335f429 --- backend/app/api/app.go | 2 + backend/app/api/handlers/v1/controller.go | 49 ++++++++++- backend/app/api/main.go | 6 +- backend/app/api/routes.go | 3 +- backend/go.mod | 2 + backend/go.sum | 11 +-- backend/internal/core/services/main_test.go | 6 +- .../services/reporting/eventbus/eventbus.go | 85 +++++++++++++++++++ backend/internal/data/repo/main_test.go | 6 +- backend/internal/data/repo/repo_items.go | 27 +++++- backend/internal/data/repo/repo_items_test.go | 6 +- backend/internal/data/repo/repo_labels.go | 33 ++++--- .../internal/data/repo/repo_labels_test.go | 10 +-- backend/internal/data/repo/repo_locations.go | 32 +++++-- .../internal/data/repo/repo_locations_test.go | 12 +-- backend/internal/data/repo/repos_all.go | 13 +-- backend/internal/web/mid/logger.go | 11 +++ frontend/components/App/ImportDialog.vue | 4 - frontend/components/Form/Password.vue | 2 +- frontend/components/Item/CreateModal.vue | 30 +++---- frontend/components/Label/CreateModal.vue | 32 +++---- frontend/components/Location/CreateModal.vue | 30 +++---- frontend/composables/use-events.ts | 38 --------- frontend/composables/use-server-events.ts | 78 +++++++++++++++++ frontend/layouts/default.vue | 50 +++-------- frontend/nuxt.config.ts | 3 +- frontend/nuxt.proxyoverride.ts | 46 ++++++++++ frontend/package.json | 1 + frontend/pages/home/table.ts | 7 +- frontend/pages/profile.vue | 28 +++--- frontend/pnpm-lock.yaml | 3 + 31 files changed, 458 insertions(+), 208 deletions(-) create mode 100644 backend/internal/core/services/reporting/eventbus/eventbus.go delete mode 100644 frontend/composables/use-events.ts create mode 100644 frontend/composables/use-server-events.ts create mode 100644 frontend/nuxt.proxyoverride.ts diff --git a/backend/app/api/app.go b/backend/app/api/app.go index 16c0ea9..73d7809 100644 --- a/backend/app/api/app.go +++ b/backend/app/api/app.go @@ -4,6 +4,7 @@ import ( "time" "github.com/hay-kot/homebox/backend/internal/core/services" + "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/sys/config" @@ -18,6 +19,7 @@ type app struct { server *server.Server repos *repo.AllRepos services *services.AllServices + bus *eventbus.EventBus } func new(conf *config.Config) *app { diff --git a/backend/app/api/handlers/v1/controller.go b/backend/app/api/handlers/v1/controller.go index 801eca8..526b8ce 100644 --- a/backend/app/api/handlers/v1/controller.go +++ b/backend/app/api/handlers/v1/controller.go @@ -1,12 +1,18 @@ package v1 import ( + "fmt" "net/http" + "github.com/google/uuid" "github.com/hay-kot/homebox/backend/internal/core/services" + "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/httpkit/server" + "github.com/rs/zerolog/log" + + "github.com/olahol/melody" ) type Results[T any] struct { @@ -49,6 +55,7 @@ type V1Controller struct { maxUploadSize int64 isDemo bool allowRegistration bool + bus *eventbus.EventBus } type ( @@ -77,11 +84,12 @@ func BaseUrlFunc(prefix string) func(s string) string { } } -func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, options ...func(*V1Controller)) *V1Controller { +func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, bus *eventbus.EventBus, options ...func(*V1Controller)) *V1Controller { ctrl := &V1Controller{ repo: repos, svc: svc, allowRegistration: true, + bus: bus, } for _, opt := range options { @@ -110,3 +118,42 @@ func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) errchain.Hand }) } } + +func (ctrl *V1Controller) HandleCacheWS() errchain.HandlerFunc { + m := melody.New() + + m.HandleConnect(func(s *melody.Session) { + auth := services.NewContext(s.Request.Context()) + s.Set("gid", auth.GID) + }) + + factory := func(e string) func(data any) { + return func(data any) { + eventData, ok := data.(eventbus.GroupMutationEvent) + if !ok { + log.Log().Msgf("invalid event data: %v", data) + return + } + + jsonStr := fmt.Sprintf(`{"event": "%s"}`, e) + + _ = m.BroadcastFilter([]byte(jsonStr), func(s *melody.Session) bool { + groupIDStr, ok := s.Get("gid") + if !ok { + return false + } + + GID := groupIDStr.(uuid.UUID) + return GID == eventData.GID + }) + } + } + + ctrl.bus.Subscribe(eventbus.EventLabelMutation, factory("label.mutation")) + ctrl.bus.Subscribe(eventbus.EventLocationMutation, factory("location.mutation")) + ctrl.bus.Subscribe(eventbus.EventItemMutation, factory("item.mutation")) + + return func(w http.ResponseWriter, r *http.Request) error { + return m.HandleRequest(w, r) + } +} diff --git a/backend/app/api/main.go b/backend/app/api/main.go index befc0de..49cb00e 100644 --- a/backend/app/api/main.go +++ b/backend/app/api/main.go @@ -14,6 +14,7 @@ import ( "github.com/go-chi/chi/v5/middleware" "github.com/hay-kot/homebox/backend/internal/core/services" + "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/migrations" "github.com/hay-kot/homebox/backend/internal/data/repo" @@ -116,8 +117,9 @@ func run(cfg *config.Config) error { return err } + app.bus = eventbus.New() app.db = c - app.repos = repo.New(c, cfg.Storage.Data) + app.repos = repo.New(c, app.bus, cfg.Storage.Data) app.services = services.New( app.repos, services.WithAutoIncrementAssetID(cfg.Options.AutoIncrementAssetID), @@ -150,6 +152,8 @@ func run(cfg *config.Config) error { // ========================================================================= // Start Reoccurring Tasks + go app.bus.Run() + go app.startBgTask(time.Duration(24)*time.Hour, func() { _, err := app.repos.AuthTokens.PurgeExpiredTokens(context.Background()) if err != nil { diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index f0b89f1..0958341 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -51,6 +51,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR v1Ctrl := v1.NewControllerV1( a.services, a.repos, + a.bus, v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize), v1.WithRegistration(a.conf.Options.AllowRegistration), v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode @@ -70,6 +71,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR a.mwRoles(RoleModeOr, authroles.RoleUser.String()), } + r.Get(v1Base("/ws/events"), chain.ToHandlerFunc(v1Ctrl.HandleCacheWS(), userMW...)) r.Get(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelf(), userMW...)) r.Put(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfUpdate(), userMW...)) r.Delete(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfDelete(), userMW...)) @@ -153,7 +155,6 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR r.Get(v1Base("/reporting/bill-of-materials"), chain.ToHandlerFunc(v1Ctrl.HandleBillOfMaterialsExport(), userMW...)) r.NotFound(chain.ToHandlerFunc(notFoundHandler())) - } func registerMimes() { diff --git a/backend/go.mod b/backend/go.mod index 454a129..4b53c7b 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -14,6 +14,7 @@ require ( github.com/gorilla/schema v1.2.0 github.com/hay-kot/httpkit v0.0.3 github.com/mattn/go-sqlite3 v1.14.17 + github.com/olahol/melody v1.1.4 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.29.1 github.com/stretchr/testify v1.8.4 @@ -43,6 +44,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/google/go-cmp v0.5.9 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/hcl/v2 v2.17.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect diff --git a/backend/go.sum b/backend/go.sum index f1be64c..c616e2b 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -410,6 +410,8 @@ github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+ github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.15.3/go.mod h1:/g/qgcoBcEXALCNZgRRisyTW0nY86++L0KbeAMXYCeY= @@ -511,8 +513,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -538,8 +538,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/olahol/melody v1.1.4 h1:RQHfKZkQmDxI0+SLZRNBCn4LiXdqxLKRGSkT8Dyoe/E= +github.com/olahol/melody v1.1.4/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= @@ -611,10 +611,7 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/backend/internal/core/services/main_test.go b/backend/internal/core/services/main_test.go index c79bfcf..b0d248d 100644 --- a/backend/internal/core/services/main_test.go +++ b/backend/internal/core/services/main_test.go @@ -6,6 +6,7 @@ import ( "os" "testing" + "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/pkgs/faker" @@ -13,7 +14,8 @@ import ( ) var ( - fk = faker.NewFaker() + fk = faker.NewFaker() + tbus = eventbus.New() tCtx = Context{} tClient *ent.Client @@ -58,7 +60,7 @@ func TestMain(m *testing.M) { } tClient = client - tRepos = repo.New(tClient, os.TempDir()+"/homebox") + tRepos = repo.New(tClient, tbus, os.TempDir()+"/homebox") tSvc = New(tRepos) defer client.Close() diff --git a/backend/internal/core/services/reporting/eventbus/eventbus.go b/backend/internal/core/services/reporting/eventbus/eventbus.go new file mode 100644 index 0000000..508e1f0 --- /dev/null +++ b/backend/internal/core/services/reporting/eventbus/eventbus.go @@ -0,0 +1,85 @@ +// / Package eventbus provides an interface for event bus. +package eventbus + +import ( + "sync" + + "github.com/google/uuid" +) + +type Event string + +const ( + EventLabelMutation Event = "label.mutation" + EventLocationMutation Event = "location.mutation" + EventItemMutation Event = "item.mutation" +) + +type GroupMutationEvent struct { + GID uuid.UUID +} + +type eventData struct { + event Event + data any +} + +type EventBus struct { + started bool + ch chan eventData + + mu sync.RWMutex + subscribers map[Event][]func(any) +} + +func New() *EventBus { + return &EventBus{ + ch: make(chan eventData, 10), + subscribers: map[Event][]func(any){ + EventLabelMutation: {}, + EventLocationMutation: {}, + EventItemMutation: {}, + }, + } +} + +func (e *EventBus) Run() { + if e.started { + panic("event bus already started") + } + + e.started = true + + for event := range e.ch { + e.mu.RLock() + arr, ok := e.subscribers[event.event] + e.mu.RUnlock() + + if !ok { + continue + } + + for _, fn := range arr { + fn(event.data) + } + } +} + +func (e *EventBus) Publish(event Event, data any) { + e.ch <- eventData{ + event: event, + data: data, + } +} + +func (e *EventBus) Subscribe(event Event, fn func(any)) { + e.mu.Lock() + defer e.mu.Unlock() + + arr, ok := e.subscribers[event] + if !ok { + panic("event not found") + } + + e.subscribers[event] = append(arr, fn) +} diff --git a/backend/internal/data/repo/main_test.go b/backend/internal/data/repo/main_test.go index cfb1630..23a4198 100644 --- a/backend/internal/data/repo/main_test.go +++ b/backend/internal/data/repo/main_test.go @@ -6,6 +6,7 @@ import ( "os" "testing" + "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/pkgs/faker" _ "github.com/mattn/go-sqlite3" @@ -13,6 +14,7 @@ import ( var ( fk = faker.NewFaker() + tbus = eventbus.New() tClient *ent.Client tRepos *AllRepos @@ -43,13 +45,15 @@ func TestMain(m *testing.M) { log.Fatalf("failed opening connection to sqlite: %v", err) } + go tbus.Run() + err = client.Schema.Create(context.Background()) if err != nil { log.Fatalf("failed creating schema resources: %v", err) } tClient = client - tRepos = New(tClient, os.TempDir()) + tRepos = New(tClient, tbus, os.TempDir()) defer client.Close() bootstrap() diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go index 3f3c11b..2431736 100644 --- a/backend/internal/data/repo/repo_items.go +++ b/backend/internal/data/repo/repo_items.go @@ -6,6 +6,7 @@ import ( "time" "github.com/google/uuid" + "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent/group" "github.com/hay-kot/homebox/backend/internal/data/ent/item" @@ -17,7 +18,8 @@ import ( ) type ItemsRepository struct { - db *ent.Client + db *ent.Client + bus *eventbus.EventBus } type ( @@ -266,6 +268,12 @@ func mapItemOut(item *ent.Item) ItemOut { } } +func (r *ItemsRepository) publishMutationEvent(GID uuid.UUID) { + if r.bus != nil { + r.bus.Publish(eventbus.EventItemMutation, eventbus.GroupMutationEvent{GID: GID}) + } +} + func (e *ItemsRepository) getOne(ctx context.Context, where ...predicate.Item) (ItemOut, error) { q := e.db.Item.Query().Where(where...) @@ -520,11 +528,18 @@ func (e *ItemsRepository) Create(ctx context.Context, gid uuid.UUID, data ItemCr return ItemOut{}, err } + e.publishMutationEvent(gid) return e.GetOne(ctx, result.ID) } func (e *ItemsRepository) Delete(ctx context.Context, id uuid.UUID) error { - return e.db.Item.DeleteOneID(id).Exec(ctx) + err := e.db.Item.DeleteOneID(id).Exec(ctx) + if err != nil { + return err + } + + e.publishMutationEvent(id) + return nil } func (e *ItemsRepository) DeleteByGroup(ctx context.Context, gid, id uuid.UUID) error { @@ -534,6 +549,12 @@ func (e *ItemsRepository) DeleteByGroup(ctx context.Context, gid, id uuid.UUID) item.ID(id), item.HasGroupWith(group.ID(gid)), ).Exec(ctx) + + if err != nil { + return err + } + + e.publishMutationEvent(gid) return err } @@ -649,6 +670,7 @@ func (e *ItemsRepository) UpdateByGroup(ctx context.Context, GID uuid.UUID, data } } + e.publishMutationEvent(GID) return e.GetOne(ctx, data.ID) } @@ -687,6 +709,7 @@ func (e *ItemsRepository) Patch(ctx context.Context, GID, ID uuid.UUID, data Ite q.SetQuantity(*data.Quantity) } + e.publishMutationEvent(GID) return q.Exec(ctx) } diff --git a/backend/internal/data/repo/repo_items_test.go b/backend/internal/data/repo/repo_items_test.go index ac2814d..ba991f9 100644 --- a/backend/internal/data/repo/repo_items_test.go +++ b/backend/internal/data/repo/repo_items_test.go @@ -39,7 +39,7 @@ func useItems(t *testing.T, len int) []ItemOut { _ = tRepos.Items.Delete(context.Background(), item.ID) } - _ = tRepos.Locations.Delete(context.Background(), location.ID) + _ = tRepos.Locations.delete(context.Background(), location.ID) }) return items @@ -123,7 +123,7 @@ func TestItemsRepository_Create(t *testing.T) { assert.NotEmpty(t, result.ID) // Cleanup - Also deletes item - err = tRepos.Locations.Delete(context.Background(), location.ID) + err = tRepos.Locations.delete(context.Background(), location.ID) assert.NoError(t, err) } @@ -147,7 +147,7 @@ func TestItemsRepository_Create_Location(t *testing.T) { assert.Equal(t, location.ID, foundItem.Location.ID) // Cleanup - Also deletes item - err = tRepos.Locations.Delete(context.Background(), location.ID) + err = tRepos.Locations.delete(context.Background(), location.ID) assert.NoError(t, err) } diff --git a/backend/internal/data/repo/repo_labels.go b/backend/internal/data/repo/repo_labels.go index ee62fd8..7814577 100644 --- a/backend/internal/data/repo/repo_labels.go +++ b/backend/internal/data/repo/repo_labels.go @@ -5,6 +5,7 @@ import ( "time" "github.com/google/uuid" + "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent/group" "github.com/hay-kot/homebox/backend/internal/data/ent/item" @@ -13,8 +14,10 @@ import ( ) type LabelRepository struct { - db *ent.Client + db *ent.Client + bus *eventbus.EventBus } + type ( LabelCreate struct { Name string `json:"name" validate:"required,min=1,max=255"` @@ -65,6 +68,12 @@ func mapLabelOut(label *ent.Label) LabelOut { } } +func (r *LabelRepository) publishMutationEvent(GID uuid.UUID) { + if r.bus != nil { + r.bus.Publish(eventbus.EventLabelMutation, eventbus.GroupMutationEvent{GID: GID}) + } +} + func (r *LabelRepository) getOne(ctx context.Context, where ...predicate.Label) (LabelOut, error) { return mapLabelOutErr(r.db.Label.Query(). Where(where...). @@ -105,6 +114,7 @@ func (r *LabelRepository) Create(ctx context.Context, groupdId uuid.UUID, data L } label.Edges.Group = &ent.Group{ID: groupdId} // bootstrap group ID + r.publishMutationEvent(groupdId) return mapLabelOut(label), err } @@ -121,25 +131,19 @@ func (r *LabelRepository) update(ctx context.Context, data LabelUpdate, where .. Save(ctx) } -func (r *LabelRepository) Update(ctx context.Context, data LabelUpdate) (LabelOut, error) { - _, err := r.update(ctx, data, label.ID(data.ID)) - if err != nil { - return LabelOut{}, err - } - - return r.GetOne(ctx, data.ID) -} - func (r *LabelRepository) UpdateByGroup(ctx context.Context, GID uuid.UUID, data LabelUpdate) (LabelOut, error) { _, err := r.update(ctx, data, label.ID(data.ID), label.HasGroupWith(group.ID(GID))) if err != nil { return LabelOut{}, err } + r.publishMutationEvent(GID) return r.GetOne(ctx, data.ID) } -func (r *LabelRepository) Delete(ctx context.Context, id uuid.UUID) error { +// delete removes the label from the database. This should only be used when +// the label's ownership is already confirmed/validated. +func (r *LabelRepository) delete(ctx context.Context, id uuid.UUID) error { return r.db.Label.DeleteOneID(id).Exec(ctx) } @@ -149,6 +153,11 @@ func (r *LabelRepository) DeleteByGroup(ctx context.Context, gid, id uuid.UUID) label.ID(id), label.HasGroupWith(group.ID(gid)), ).Exec(ctx) + if err != nil { + return err + } - return err + r.publishMutationEvent(gid) + + return nil } diff --git a/backend/internal/data/repo/repo_labels_test.go b/backend/internal/data/repo/repo_labels_test.go index 691b915..f3b331c 100644 --- a/backend/internal/data/repo/repo_labels_test.go +++ b/backend/internal/data/repo/repo_labels_test.go @@ -28,7 +28,7 @@ func useLabels(t *testing.T, len int) []LabelOut { t.Cleanup(func() { for _, item := range labels { - _ = tRepos.Labels.Delete(context.Background(), item.ID) + _ = tRepos.Labels.delete(context.Background(), item.ID) } }) @@ -62,7 +62,7 @@ func TestLabelRepository_Create(t *testing.T) { assert.NoError(t, err) assert.Equal(t, loc.ID, foundLoc.ID) - err = tRepos.Labels.Delete(context.Background(), loc.ID) + err = tRepos.Labels.delete(context.Background(), loc.ID) assert.NoError(t, err) } @@ -76,7 +76,7 @@ func TestLabelRepository_Update(t *testing.T) { Description: fk.Str(100), } - update, err := tRepos.Labels.Update(context.Background(), updateData) + update, err := tRepos.Labels.UpdateByGroup(context.Background(), tGroup.ID, updateData) assert.NoError(t, err) foundLoc, err := tRepos.Labels.GetOne(context.Background(), loc.ID) @@ -86,7 +86,7 @@ func TestLabelRepository_Update(t *testing.T) { assert.Equal(t, update.Name, foundLoc.Name) assert.Equal(t, update.Description, foundLoc.Description) - err = tRepos.Labels.Delete(context.Background(), loc.ID) + err = tRepos.Labels.delete(context.Background(), loc.ID) assert.NoError(t, err) } @@ -94,7 +94,7 @@ func TestLabelRepository_Delete(t *testing.T) { loc, err := tRepos.Labels.Create(context.Background(), tGroup.ID, labelFactory()) assert.NoError(t, err) - err = tRepos.Labels.Delete(context.Background(), loc.ID) + err = tRepos.Labels.delete(context.Background(), loc.ID) assert.NoError(t, err) _, err = tRepos.Labels.GetOne(context.Background(), loc.ID) diff --git a/backend/internal/data/repo/repo_locations.go b/backend/internal/data/repo/repo_locations.go index 28e3968..8c0ebfc 100644 --- a/backend/internal/data/repo/repo_locations.go +++ b/backend/internal/data/repo/repo_locations.go @@ -6,6 +6,7 @@ import ( "time" "github.com/google/uuid" + "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent/group" "github.com/hay-kot/homebox/backend/internal/data/ent/item" @@ -14,7 +15,8 @@ import ( ) type LocationRepository struct { - db *ent.Client + db *ent.Client + bus *eventbus.EventBus } type ( @@ -90,6 +92,12 @@ func mapLocationOut(location *ent.Location) LocationOut { } } +func (r *LocationRepository) publishMutationEvent(GID uuid.UUID) { + if r.bus != nil { + r.bus.Publish(eventbus.EventLocationMutation, eventbus.GroupMutationEvent{GID: GID}) + } +} + type LocationQuery struct { FilterChildren bool `json:"filterChildren" schema:"filterChildren"` } @@ -190,6 +198,7 @@ func (r *LocationRepository) Create(ctx context.Context, GID uuid.UUID, data Loc } location.Edges.Group = &ent.Group{ID: GID} // bootstrap group ID + r.publishMutationEvent(GID) return mapLocationOut(location), nil } @@ -213,20 +222,29 @@ func (r *LocationRepository) update(ctx context.Context, data LocationUpdate, wh return r.Get(ctx, data.ID) } -func (r *LocationRepository) Update(ctx context.Context, data LocationUpdate) (LocationOut, error) { - return r.update(ctx, data, location.ID(data.ID)) -} - func (r *LocationRepository) UpdateByGroup(ctx context.Context, GID, ID uuid.UUID, data LocationUpdate) (LocationOut, error) { - return r.update(ctx, data, location.ID(ID), location.HasGroupWith(group.ID(GID))) + v, err := r.update(ctx, data, location.ID(ID), location.HasGroupWith(group.ID(GID))) + if err != nil { + return LocationOut{}, err + } + + r.publishMutationEvent(GID) + return v, err } -func (r *LocationRepository) Delete(ctx context.Context, ID uuid.UUID) error { +// delete should only be used after checking that the location is owned by the +// group. Otherwise, use DeleteByGroup +func (r *LocationRepository) delete(ctx context.Context, ID uuid.UUID) error { return r.db.Location.DeleteOneID(ID).Exec(ctx) } func (r *LocationRepository) DeleteByGroup(ctx context.Context, GID, ID uuid.UUID) error { _, err := r.db.Location.Delete().Where(location.ID(ID), location.HasGroupWith(group.ID(GID))).Exec(ctx) + if err != nil { + return err + } + r.publishMutationEvent(GID) + return err } diff --git a/backend/internal/data/repo/repo_locations_test.go b/backend/internal/data/repo/repo_locations_test.go index 8840b51..c649bcb 100644 --- a/backend/internal/data/repo/repo_locations_test.go +++ b/backend/internal/data/repo/repo_locations_test.go @@ -30,7 +30,7 @@ func useLocations(t *testing.T, len int) []LocationOut { t.Cleanup(func() { for _, loc := range out { - err := tRepos.Locations.Delete(context.Background(), loc.ID) + err := tRepos.Locations.delete(context.Background(), loc.ID) if err != nil { assert.True(t, ent.IsNotFound(err)) } @@ -49,7 +49,7 @@ func TestLocationRepository_Get(t *testing.T) { assert.NoError(t, err) assert.Equal(t, loc.ID, foundLoc.ID) - err = tRepos.Locations.Delete(context.Background(), loc.ID) + err = tRepos.Locations.delete(context.Background(), loc.ID) assert.NoError(t, err) } @@ -83,7 +83,7 @@ func TestLocationRepository_Create(t *testing.T) { assert.NoError(t, err) assert.Equal(t, loc.ID, foundLoc.ID) - err = tRepos.Locations.Delete(context.Background(), loc.ID) + err = tRepos.Locations.delete(context.Background(), loc.ID) assert.NoError(t, err) } @@ -96,7 +96,7 @@ func TestLocationRepository_Update(t *testing.T) { Description: fk.Str(100), } - update, err := tRepos.Locations.Update(context.Background(), updateData) + update, err := tRepos.Locations.UpdateByGroup(context.Background(), tGroup.ID, updateData.ID, updateData) assert.NoError(t, err) foundLoc, err := tRepos.Locations.Get(context.Background(), loc.ID) @@ -106,14 +106,14 @@ func TestLocationRepository_Update(t *testing.T) { assert.Equal(t, update.Name, foundLoc.Name) assert.Equal(t, update.Description, foundLoc.Description) - err = tRepos.Locations.Delete(context.Background(), loc.ID) + err = tRepos.Locations.delete(context.Background(), loc.ID) assert.NoError(t, err) } func TestLocationRepository_Delete(t *testing.T) { loc := useLocations(t, 1)[0] - err := tRepos.Locations.Delete(context.Background(), loc.ID) + err := tRepos.Locations.delete(context.Background(), loc.ID) assert.NoError(t, err) _, err = tRepos.Locations.Get(context.Background(), loc.ID) diff --git a/backend/internal/data/repo/repos_all.go b/backend/internal/data/repo/repos_all.go index 2a3cf27..f9e6197 100644 --- a/backend/internal/data/repo/repos_all.go +++ b/backend/internal/data/repo/repos_all.go @@ -1,6 +1,9 @@ package repo -import "github.com/hay-kot/homebox/backend/internal/data/ent" +import ( + "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" + "github.com/hay-kot/homebox/backend/internal/data/ent" +) // AllRepos is a container for all the repository interfaces type AllRepos struct { @@ -16,14 +19,14 @@ type AllRepos struct { Notifiers *NotifierRepository } -func New(db *ent.Client, root string) *AllRepos { +func New(db *ent.Client, bus *eventbus.EventBus, root string) *AllRepos { return &AllRepos{ Users: &UserRepository{db}, AuthTokens: &TokenRepository{db}, Groups: NewGroupRepository(db), - Locations: &LocationRepository{db}, - Labels: &LabelRepository{db}, - Items: &ItemsRepository{db}, + Locations: &LocationRepository{db, bus}, + Labels: &LabelRepository{db, bus}, + Items: &ItemsRepository{db, bus}, Docs: &DocumentRepository{db, root}, Attachments: &AttachmentRepo{db}, MaintEntry: &MaintenanceEntryRepository{db}, diff --git a/backend/internal/web/mid/logger.go b/backend/internal/web/mid/logger.go index d087c68..0be4722 100644 --- a/backend/internal/web/mid/logger.go +++ b/backend/internal/web/mid/logger.go @@ -1,6 +1,9 @@ package mid import ( + "bufio" + "errors" + "net" "net/http" "github.com/go-chi/chi/v5/middleware" @@ -17,6 +20,14 @@ func (s *spy) WriteHeader(status int) { s.ResponseWriter.WriteHeader(status) } +func (s *spy) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hj, ok := s.ResponseWriter.(http.Hijacker) + if !ok { + return nil, nil, errors.New("response writer does not support hijacking") + } + return hj.Hijack() +} + func Logger(l zerolog.Logger) func(http.Handler) http.Handler { return func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/frontend/components/App/ImportDialog.vue b/frontend/components/App/ImportDialog.vue index adfe993..31da62d 100644 --- a/frontend/components/App/ImportDialog.vue +++ b/frontend/components/App/ImportDialog.vue @@ -86,8 +86,6 @@ importRef.value?.click(); } - const eventBus = useEventBus(); - async function submitCsvFile() { if (!importCsv.value) { toast.error("Please select a file to import."); @@ -111,8 +109,6 @@ importRef.value.value = ""; } - eventBus.emit(EventTypes.InvalidStores); - toast.success("Import successful!"); } diff --git a/frontend/components/Form/Password.vue b/frontend/components/Form/Password.vue index 0c03c91..6ea5313 100644 --- a/frontend/components/Form/Password.vue +++ b/frontend/components/Form/Password.vue @@ -15,7 +15,7 @@ diff --git a/frontend/components/Label/CreateModal.vue b/frontend/components/Label/CreateModal.vue index 4b0ee48..da1a594 100644 --- a/frontend/components/Label/CreateModal.vue +++ b/frontend/components/Label/CreateModal.vue @@ -1,7 +1,7 @@ @@ -69,7 +67,13 @@ const api = useUserApi(); const toast = useNotifier(); - async function create(close: boolean) { + const { shift } = useMagicKeys(); + + async function create(close = true) { + if (shift.value) { + close = false; + } + const { error, data } = await api.labels.create(form); if (error) { toast.error("Couldn't create label"); @@ -84,16 +88,4 @@ navigateTo(`/label/${data.id}`); } } - - async function keySubmit(e: KeyboardEvent) { - // Shift + Enter - if (e.shiftKey && e.key === "Enter") { - e.preventDefault(); - await create(false); - focused.value = true; - } else if (e.key === "Enter") { - e.preventDefault(); - await create(true); - } - } diff --git a/frontend/components/Location/CreateModal.vue b/frontend/components/Location/CreateModal.vue index f87715a..d0dadbb 100644 --- a/frontend/components/Location/CreateModal.vue +++ b/frontend/components/Location/CreateModal.vue @@ -1,7 +1,7 @@