forked from mirrors/homebox
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
This commit is contained in:
parent
cceec06148
commit
2cbcc8bb1d
31 changed files with 458 additions and 208 deletions
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue