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:
Hayden 2023-08-02 13:00:57 -08:00 committed by GitHub
parent cceec06148
commit 2cbcc8bb1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 458 additions and 208 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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