From 021f67ea4bc877ab27a20fd042c28683fde82236 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sun, 16 Jun 2019 10:32:34 +0200 Subject: [PATCH] Changes to change system. --- .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/inspectionProfiles/Project_Default.xml | 6 + cmd/rpdata-server/main.go | 7 +- database/mongodb/changes.go | 104 +++++++++++++++ database/mongodb/db.go | 23 ++++ graph2/complexity.go | 3 +- graph2/gqlgen.yml | 2 +- graph2/resolvers/changes.go | 30 ++--- graph2/schema/root.gql | 2 +- graph2/schema/types/Change.gql | 5 +- internal/auth/token.go | 32 +++-- internal/notifier/notifier.go | 44 ++++++ models/change.go | 128 +++++++++++++++++- models/changes/db.go | 18 +-- repositories/change.go | 13 ++ repositories/repository.go | 1 + services/changes.go | 133 +++++++++++++++++++ services/characters.go | 25 ++-- services/services.go | 9 +- 19 files changed, 503 insertions(+), 87 deletions(-) create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 database/mongodb/changes.go create mode 100644 internal/notifier/notifier.go create mode 100644 repositories/change.go create mode 100644 services/changes.go diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..1b22473 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/cmd/rpdata-server/main.go b/cmd/rpdata-server/main.go index fd3fdac..e235cb6 100644 --- a/cmd/rpdata-server/main.go +++ b/cmd/rpdata-server/main.go @@ -16,7 +16,6 @@ import ( "git.aiterp.net/rpdata/api/internal/loader" "git.aiterp.net/rpdata/api/internal/store" "git.aiterp.net/rpdata/api/models" - "git.aiterp.net/rpdata/api/models/changes" "git.aiterp.net/rpdata/api/models/logs" "git.aiterp.net/rpdata/api/services" "github.com/99designs/gqlgen/handler" @@ -52,13 +51,13 @@ func main() { log.Println("Characters updated") }() - go logListedChanges() + go logListedChanges(services.Changes) log.Fatal(http.ListenAndServe(":8081", nil)) } -func logListedChanges() { - sub := changes.Subscribe(context.Background(), nil, true) +func logListedChanges(changes *services.ChangeService) { + sub := changes.Subscribe(context.Background(), models.ChangeFilter{PassAll: true}) for change := range sub { log.Printf("Change: Author=%#+v Model=%#+v Op=%#+v", change.Author, change.Model, change.Op) diff --git a/database/mongodb/changes.go b/database/mongodb/changes.go new file mode 100644 index 0000000..3c3cdb9 --- /dev/null +++ b/database/mongodb/changes.go @@ -0,0 +1,104 @@ +package mongodb + +import ( + "context" + "git.aiterp.net/rpdata/api/models" + "git.aiterp.net/rpdata/api/repositories" + "github.com/globalsign/mgo" + "github.com/globalsign/mgo/bson" + "strconv" + "time" +) + +type changeRepository struct { + changes *mgo.Collection + idCounter *counter +} + +func (r *changeRepository) Find(ctx context.Context, id string) (*models.Change, error) { + change := new(models.Change) + err := r.changes.FindId(id).One(change) + if err != nil { + return nil, err + } + + return change, nil +} + +func (r *changeRepository) List(ctx context.Context, filter models.ChangeFilter) ([]*models.Change, error) { + query := bson.M{} + limit := 0 + if filter.EarliestDate != nil && !filter.EarliestDate.IsZero() { + query["date"] = bson.M{"$gte": *filter.EarliestDate} + } + if len(filter.Keys) > 0 { + query["keys"] = bson.M{"$in": filter.Keys} + } + if filter.Author != nil && *filter.Author != "" { + query["author"] = *filter.Author + } + if filter.Limit != nil { + limit = *filter.Limit + } + + initialSize := 64 + if limit > 0 && limit < 256 { + initialSize = limit + } + + changes := make([]*models.Change, 0, initialSize) + err := r.changes.Find(query).All(&changes) + if err != nil { + return nil, err + } + + return changes, nil +} + +func (r *changeRepository) Insert(ctx context.Context, change models.Change) (*models.Change, error) { + next, err := r.idCounter.Increment(1) + if err != nil { + return nil, err + } + + change.ID = "Change_" + strconv.Itoa(next) + + err = r.changes.Insert(change) + if err != nil { + return nil, err + } + + return &change, nil +} + +func (r *changeRepository) Remove(ctx context.Context, change models.Change) error { + return r.changes.RemoveId(change.ID) +} + +func newChangeRepository(db *mgo.Database) (repositories.ChangeRepository, error) { + collection := db.C("common.changes") + + err := collection.EnsureIndex(mgo.Index{ + Name: "expiry", + Key: []string{"date"}, + ExpireAfter: time.Hour * 2400, // 100 days + }) + if err != nil { + return nil, err + } + + err = collection.EnsureIndexKey("author") + if err != nil { + return nil, err + } + + err = collection.EnsureIndexKey("keys") + if err != nil { + return nil, err + } + + return &changeRepository{ + changes: collection, + idCounter: newCounter(db, "auto_increment", "Change"), + }, nil +} diff --git a/database/mongodb/db.go b/database/mongodb/db.go index 8e42b20..5a19e03 100644 --- a/database/mongodb/db.go +++ b/database/mongodb/db.go @@ -38,8 +38,15 @@ func Init(cfg config.Database) (bundle *repositories.Bundle, closeFn func() erro return nil, nil, err } + changes, err := newChangeRepository(db) + if err != nil { + session.Close() + return nil, nil, err + } + bundle = &repositories.Bundle{ Characters: characters, + Changes: changes, Tags: newTagRepository(db), } @@ -73,6 +80,22 @@ func (c *counter) WithName(name string) *counter { } } +func (c *counter) WithCategory(category string) *counter { + return &counter{ + coll: c.coll, + category: category, + name: c.name, + } +} + +func (c *counter) With(category, name string) *counter { + return &counter{ + coll: c.coll, + category: category, + name: name, + } +} + func (c *counter) Increment(amount int) (int, error) { type counterDoc struct { ID string `bson:"_id"` diff --git a/graph2/complexity.go b/graph2/complexity.go index b3f4e1c..f68ec85 100644 --- a/graph2/complexity.go +++ b/graph2/complexity.go @@ -3,7 +3,6 @@ package graph2 import ( "git.aiterp.net/rpdata/api/graph2/graphcore" "git.aiterp.net/rpdata/api/models" - "git.aiterp.net/rpdata/api/models/changes" "git.aiterp.net/rpdata/api/models/channels" "git.aiterp.net/rpdata/api/models/files" "git.aiterp.net/rpdata/api/models/logs" @@ -71,7 +70,7 @@ func complexity() (cr graphcore.ComplexityRoot) { cr.Query.Files = func(childComplexity int, filter *files.Filter) int { return childComplexity + listComplexity } - cr.Query.Changes = func(childComplexity int, filter *changes.Filter) int { + cr.Query.Changes = func(childComplexity int, filter *models.ChangeFilter) int { return childComplexity + listComplexity } cr.Query.Token = func(childComplexity int) int { diff --git a/graph2/gqlgen.yml b/graph2/gqlgen.yml index a28d8f5..a23d927 100644 --- a/graph2/gqlgen.yml +++ b/graph2/gqlgen.yml @@ -62,7 +62,7 @@ models: ChangeKeyInput: # It's the same as ChangeKey model: git.aiterp.net/rpdata/api/models.ChangeKey ChangesFilter: - model: git.aiterp.net/rpdata/api/models/changes.Filter + model: git.aiterp.net/rpdata/api/models.ChangeFilter Token: model: git.aiterp.net/rpdata/api/models.Token User: diff --git a/graph2/resolvers/changes.go b/graph2/resolvers/changes.go index d8379a4..a87e123 100644 --- a/graph2/resolvers/changes.go +++ b/graph2/resolvers/changes.go @@ -2,39 +2,25 @@ package resolvers import ( "context" - "errors" - "git.aiterp.net/rpdata/api/models" - "git.aiterp.net/rpdata/api/models/changes" ) /// Queries -func (r *queryResolver) Changes(ctx context.Context, filter *changes.Filter) ([]*models.Change, error) { - changes, err := changes.List(filter) - if err != nil { - return nil, err - } - - changes2 := make([]*models.Change, len(changes)) - for i := range changes { - changes2[i] = &changes[i] +func (r *queryResolver) Changes(ctx context.Context, filter *models.ChangeFilter) ([]*models.Change, error) { + if filter == nil { + filter = &models.ChangeFilter{} } - return changes2, nil + return r.s.Changes.List(ctx, *filter) } /// Subscriptions -func (r *subscriptionResolver) Changes(ctx context.Context, keys []*models.ChangeKey) (<-chan *models.Change, error) { - if len(keys) == 0 { - return nil, errors.New("At least one key is required for a subscription") - } - - keys2 := make([]models.ChangeKey, len(keys)) - for i := range keys { - keys2[i] = *keys[i] +func (r *subscriptionResolver) Changes(ctx context.Context, filter *models.ChangeFilter) (<-chan *models.Change, error) { + if filter == nil { + filter = &models.ChangeFilter{} } - return changes.Subscribe(ctx, keys2, false), nil + return r.s.Changes.Subscribe(ctx, *filter), nil } diff --git a/graph2/schema/root.gql b/graph2/schema/root.gql index bb00b30..5e9a03b 100644 --- a/graph2/schema/root.gql +++ b/graph2/schema/root.gql @@ -164,7 +164,7 @@ type Subscription { """ Changes subscribes to the changes matching the following keys. """ - changes(keys: [ChangeKeyInput!]!): Change! + changes(filter: ChangesFilter): Change! } # A Time represents a RFC3339 encoded date with up to millisecond precision. diff --git a/graph2/schema/types/Change.gql b/graph2/schema/types/Change.gql index 28326d7..e702274 100644 --- a/graph2/schema/types/Change.gql +++ b/graph2/schema/types/Change.gql @@ -73,9 +73,12 @@ input ChangesFilter { "The keys to query for." keys: [ChangeKeyInput!] + "Only show changes by this author" + author: String + "Only show changes more recent than this date." earliestDate: Time - "Limit the amount of results." + "Limit the amount of results. This even goes for subscriptions!" limit: Int } \ No newline at end of file diff --git a/internal/auth/token.go b/internal/auth/token.go index 209431d..73036c7 100644 --- a/internal/auth/token.go +++ b/internal/auth/token.go @@ -114,20 +114,24 @@ func CheckToken(tokenString string) (token models.Token, err error) { return models.Token{}, ErrDeletedUser } - acceptedPermissions := make([]string, 0, 8) - for _, permission := range permissions { - found := false - - for _, userPermission := range user.Permissions { - if permission == userPermission { - found = true - break + acceptedPermissions := make([]string, 0, len(user.Permissions)) + if len(permissions) > 0 { + for _, permission := range permissions { + found := false + + for _, userPermission := range user.Permissions { + if permission == userPermission { + found = true + break + } } - } - if found { - acceptedPermissions = append(acceptedPermissions, permission) + if found { + acceptedPermissions = append(acceptedPermissions, permission) + } } + } else { + acceptedPermissions = append(acceptedPermissions, user.Permissions...) } return models.Token{UserID: user.ID, Permissions: acceptedPermissions}, nil @@ -153,10 +157,10 @@ func parseClaims(jwtClaims jwt.Claims) (userid string, permissions []string, err permissions = append(permissions, permission) } } - } - if len(permissions) == 0 { - return "", nil, ErrInvalidClaims + if len(permissions) == 0 { + return "", nil, ErrInvalidClaims + } } return diff --git a/internal/notifier/notifier.go b/internal/notifier/notifier.go new file mode 100644 index 0000000..d1958e2 --- /dev/null +++ b/internal/notifier/notifier.go @@ -0,0 +1,44 @@ +package notifier + +import ( + "context" + "sync" +) + +// A Notifier is a synchronization primitive for waking upp all listeners at once. +type Notifier struct { + mutex sync.Mutex + ch chan struct{} +} + +// Broadcast wakes all listeners if there are any. +func (notifier *Notifier) Broadcast() { + notifier.mutex.Lock() + if notifier.ch != nil { + close(notifier.ch) + notifier.ch = nil + } + notifier.mutex.Unlock() +} + +// C gets the channel that'll close on the next notification. +func (notifier *Notifier) C() <-chan struct{} { + notifier.mutex.Lock() + if notifier.ch == nil { + notifier.ch = make(chan struct{}) + } + ch := notifier.ch + notifier.mutex.Unlock() + + return ch +} + +// Wait waits for the next `Broadcast` call, or the context's termination. +func (notifier *Notifier) Wait(ctx context.Context) error { + select { + case <-notifier.C(): + return nil + case <-ctx.Done(): + return ctx.Err() + } +} diff --git a/models/change.go b/models/change.go index 1bd9601..be22981 100644 --- a/models/change.go +++ b/models/change.go @@ -1,6 +1,9 @@ package models -import "time" +import ( + "reflect" + "time" +) // Change represents a change in the rpdata history through the API. type Change struct { @@ -22,15 +25,55 @@ type Change struct { Comments []Comment `bson:"comments"` } -// ChangeKey is a key for a change that can be used when subscribing to them. -type ChangeKey struct { - Model ChangeModel `bson:"model"` - ID string `bson:"id"` +// AddObject adds the model into the appropriate array. +func (change *Change) AddObject(object interface{}) bool { + if v := reflect.ValueOf(object); v.Kind() == reflect.Ptr { + return change.AddObject(v.Elem().Interface()) + } + + switch object := object.(type) { + case Log: + change.Logs = append(change.Logs, object) + case []Log: + change.Logs = append(change.Logs, object...) + case Character: + change.Characters = append(change.Characters, object) + case []Character: + change.Characters = append(change.Characters, object...) + case Channel: + change.Channels = append(change.Channels, object) + case []Channel: + change.Channels = append(change.Channels, object...) + case Post: + change.Posts = append(change.Posts, object) + case []Post: + change.Posts = append(change.Posts, object...) + case Story: + change.Stories = append(change.Stories, object) + case []Story: + change.Stories = append(change.Stories, object...) + case Tag: + change.Tags = append(change.Tags, object) + case []Tag: + change.Tags = append(change.Tags, object...) + case Chapter: + change.Chapters = append(change.Chapters, object) + case []Chapter: + change.Chapters = append(change.Chapters, object...) + case Comment: + change.Comments = append(change.Comments, object) + case []Comment: + change.Comments = append(change.Comments, object...) + default: + return false + } + + return true } // Objects makes a combined, mixed array of all the models stored in this change. func (change *Change) Objects() []interface{} { - data := make([]interface{}, 0, len(change.Logs)+len(change.Characters)+len(change.Channels)+len(change.Posts)+len(change.Stories)+len(change.Tags)+len(change.Chapters)+len(change.Comments)) + data := make([]interface{}, 0, 8) for _, log := range change.Logs { data = append(data, &log) @@ -59,3 +102,76 @@ func (change *Change) Objects() []interface{} { return data } + +func (change *Change) PassesFilter(filter ChangeFilter) bool { + if filter.PassAll { + return true + } + + if filter.Author != nil && change.Author != *filter.Author { + return false + } + + // For unlisted changes, pass it only if the filter refers to the specific index. + if !change.Listed { + hasSpecificKey := false + + KeyFindLoop: + for _, key := range filter.Keys { + if key.ID == "*" { + continue + } + + for _, changeKey := range change.Keys { + if changeKey.Model == key.Model && changeKey.ID == key.ID { + hasSpecificKey = true + break KeyFindLoop + } + } + } + + if !hasSpecificKey { + return false + } + } + + if filter.EarliestDate != nil && filter.EarliestDate.Before(change.Date) { + return false + } + + if len(filter.Keys) > 0 { + foundKey := false + + KeyFindLoop2: + for _, key := range filter.Keys { + for _, changeKey := range change.Keys { + if changeKey == key { + foundKey = true + break KeyFindLoop2 + } + } + } + + if !foundKey { + return false + } + } + + return true +} + +// ChangeKey is a key for a change that can be used when subscribing to them. +type ChangeKey struct { + Model ChangeModel `bson:"model"` + ID string `bson:"id"` +} + +// ChangeFilter is a filter for listing changes. +type ChangeFilter struct { + Keys []ChangeKey + EarliestDate *time.Time + Author *string + Limit *int + + PassAll bool // DO NOT EXPOSE +} diff --git a/models/changes/db.go b/models/changes/db.go index 4564511..53eff29 100644 --- a/models/changes/db.go +++ b/models/changes/db.go @@ -1,14 +1,11 @@ package changes import ( - "log" - "sync" - "time" - "git.aiterp.net/rpdata/api/internal/store" "git.aiterp.net/rpdata/api/models" "github.com/globalsign/mgo" "github.com/globalsign/mgo/bson" + "sync" ) var collection *mgo.Collection @@ -24,18 +21,5 @@ func list(query bson.M, limit int) ([]models.Change, error) { func init() { store.HandleInit(func(db *mgo.Database) { collection = db.C("common.changes") - - collection.EnsureIndexKey("date") - collection.EnsureIndexKey("author") - collection.EnsureIndexKey("keys") - - err := collection.EnsureIndex(mgo.Index{ - Name: "expiry", - Key: []string{"date"}, - ExpireAfter: time.Hour * 2400, // 100 days - }) - if err != nil { - log.Fatalln(err) - } }) } diff --git a/repositories/change.go b/repositories/change.go new file mode 100644 index 0000000..0ff3e22 --- /dev/null +++ b/repositories/change.go @@ -0,0 +1,13 @@ +package repositories + +import ( + "context" + "git.aiterp.net/rpdata/api/models" +) + +type ChangeRepository interface { + Find(ctx context.Context, id string) (*models.Change, error) + List(ctx context.Context, filter models.ChangeFilter) ([]*models.Change, error) + Insert(ctx context.Context, change models.Change) (*models.Change, error) + Remove(ctx context.Context, change models.Change) error +} diff --git a/repositories/repository.go b/repositories/repository.go index 3661dfe..1005c9a 100644 --- a/repositories/repository.go +++ b/repositories/repository.go @@ -5,6 +5,7 @@ import "errors" // A Bundle is a set of repositories. type Bundle struct { Characters CharacterRepository + Changes ChangeRepository Tags TagRepository } diff --git a/services/changes.go b/services/changes.go new file mode 100644 index 0000000..1b37220 --- /dev/null +++ b/services/changes.go @@ -0,0 +1,133 @@ +package services + +import ( + "context" + "git.aiterp.net/rpdata/api/internal/auth" + "git.aiterp.net/rpdata/api/internal/notifier" + "git.aiterp.net/rpdata/api/models" + "git.aiterp.net/rpdata/api/repositories" + "log" + "sync" + "time" +) + +type ChangeService struct { + changes repositories.ChangeRepository + + mutex sync.RWMutex + buffer []models.Change + offset uint64 + notifier notifier.Notifier + submitQueue chan *models.Change + loopStarted bool +} + +func (s *ChangeService) Find(ctx context.Context, id string) (*models.Change, error) { + return s.changes.Find(ctx, id) +} + +func (s *ChangeService) List(ctx context.Context, filter models.ChangeFilter) ([]*models.Change, error) { + return s.changes.List(ctx, filter) +} + +func (s *ChangeService) Submit(ctx context.Context, model models.ChangeModel, op string, listed bool, keys []models.ChangeKey, objects ...interface{}) { + token := auth.TokenFromContext(ctx) + if token == nil { + panic("no token!") + } + + change := &models.Change{ + Model: model, + Op: op, + Author: token.UserID, + Listed: listed, + Keys: keys, + } + + for _, obj := range objects { + if !change.AddObject(obj) { + log.Printf("Cannot add object of type %T to change", obj) + } + } + + s.mutex.Lock() + if !s.loopStarted { + s.loopStarted = true + s.submitQueue = make(chan *models.Change, 64) + go s.loop() + } + s.mutex.Unlock() + + s.submitQueue <- change +} + +func (s *ChangeService) Subscribe(ctx context.Context, filter models.ChangeFilter) <-chan *models.Change { + channel := make(chan *models.Change) + + go func() { + defer close(channel) + + s.mutex.RLock() + pos := s.offset + uint64(len(s.buffer)) + slice := make([]models.Change, 32) + s.mutex.RUnlock() + + count := 0 + + for { + s.mutex.RLock() + nextPos := s.offset + uint64(len(s.buffer)) + length := nextPos - pos + if length > 0 { + index := pos - s.offset + + pos = nextPos + copy(slice, s.buffer[index:]) + } + ch := s.notifier.C() + s.mutex.RUnlock() + + for _, change := range slice[:length] { + if change.PassesFilter(filter) { + channel <- &change + + count++ + if filter.Limit != nil && count == *filter.Limit { + return + } + } + } + + select { + case <-ch: + case <-ctx.Done(): + return + } + } + }() + + return channel +} + +func (s *ChangeService) loop() { + for change := range s.submitQueue { + timeout, cancel := context.WithTimeout(context.Background(), time.Second*15) + + change, err := s.changes.Insert(timeout, *change) + if err != nil { + log.Println("Failed to submit change:") + } + + s.mutex.Lock() + s.buffer = append(s.buffer, *change) + if len(s.buffer) > 16 { + copy(s.buffer, s.buffer[8:]) + s.buffer = s.buffer[:len(s.buffer)-8] + s.offset += 8 + } + s.mutex.Unlock() + s.notifier.Broadcast() + + cancel() + } +} diff --git a/services/characters.go b/services/characters.go index 067d527..f4431b2 100644 --- a/services/characters.go +++ b/services/characters.go @@ -6,7 +6,6 @@ import ( "git.aiterp.net/rpdata/api/internal/auth" "git.aiterp.net/rpdata/api/models" "git.aiterp.net/rpdata/api/models/changekeys" - "git.aiterp.net/rpdata/api/models/changes" "git.aiterp.net/rpdata/api/repositories" "git.aiterp.net/rpdata/api/services/loaders" "sort" @@ -14,8 +13,9 @@ import ( ) type CharacterService struct { - characters repositories.CharacterRepository - loader *loaders.CharacterLoader + characters repositories.CharacterRepository + loader *loaders.CharacterLoader + changeService *ChangeService } // Find uses the loader to find the character by the ID. @@ -101,8 +101,7 @@ func (s *CharacterService) Create(ctx context.Context, nick, name, shortName, au return nil, err } - //TODO: New change submit system - go changes.Submit("Character", "add", token.UserID, true, changekeys.Listed(character), character) + s.changeService.Submit(ctx, "Character", "add", true, changekeys.Listed(character), character) return character, nil } @@ -130,9 +129,7 @@ func (s *CharacterService) Update(ctx context.Context, id string, name, shortNam s.loader.Clear(character.ID) s.loader.Prime(character.ID, character) - //TODO: New change submit system - token := auth.TokenFromContext(ctx) - go changes.Submit("Character", "edit", token.UserID, true, changekeys.Listed(character), character) + s.changeService.Submit(ctx, "Character", "edit", true, changekeys.Listed(character), character) return character, nil } @@ -153,9 +150,7 @@ func (s *CharacterService) AddNick(ctx context.Context, id string, nick string) return nil, err } - //TODO: New change submit system - token := auth.TokenFromContext(ctx) - go changes.Submit("Character", "edit", token.UserID, true, changekeys.Listed(character), character) + s.changeService.Submit(ctx, "Character", "edit", true, changekeys.Listed(character), character) return character, nil } @@ -176,9 +171,7 @@ func (s *CharacterService) RemoveNick(ctx context.Context, id string, nick strin return nil, err } - //TODO: New change submit system - token := auth.TokenFromContext(ctx) - go changes.Submit("Character", "edit", token.UserID, true, changekeys.Listed(character), character) + s.changeService.Submit(ctx, "Character", "edit", true, changekeys.Listed(character), character) return character, nil } @@ -199,9 +192,7 @@ func (s *CharacterService) Delete(ctx context.Context, id string) (*models.Chara return nil, err } - //TODO: New change submit system - token := auth.TokenFromContext(ctx) - go changes.Submit("Character", "remove", token.UserID, true, changekeys.Listed(character), character) + s.changeService.Submit(ctx, "Character", "remove", true, changekeys.Listed(character), character) return character, nil } diff --git a/services/services.go b/services/services.go index 45c7f3f..23173c7 100644 --- a/services/services.go +++ b/services/services.go @@ -9,16 +9,21 @@ import ( type Bundle struct { Tags *TagService Characters *CharacterService + Changes *ChangeService } // NewBundle creates a new bundle. func NewBundle(repos *repositories.Bundle) *Bundle { bundle := &Bundle{} + bundle.Changes = &ChangeService{ + changes: repos.Changes, + } bundle.Tags = &TagService{tags: repos.Tags} bundle.Characters = &CharacterService{ - characters: repos.Characters, - loader: loaders.CharacterLoaderFromRepository(repos.Characters), + characters: repos.Characters, + loader: loaders.CharacterLoaderFromRepository(repos.Characters), + changeService: bundle.Changes, } return bundle