From cee370c2acbdf3eb3ff5a3f0d2abd495782852c3 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sun, 28 Oct 2018 12:18:17 +0100 Subject: [PATCH] models: Changed change logging system to be allow for better querying in the future. --- graph2/queries/channel.go | 5 ++-- graph2/queries/character.go | 11 +++---- graph2/queries/log.go | 7 +++-- graph2/queries/post.go | 27 ++++++++++++------ models/change.go | 18 ++++++++---- models/changekeys/all.go | 8 ++++++ models/changekeys/listed.go | 10 +++++++ models/changekeys/many.go | 27 ++++++++++++++++++ models/changekeys/many_test.go | 52 ++++++++++++++++++++++++++++++++++ models/changekeys/one.go | 29 +++++++++++++++++++ models/changekeys/one_test.go | 42 +++++++++++++++++++++++++++ models/changes/db.go | 1 + models/changes/submit.go | 12 ++++++-- 13 files changed, 224 insertions(+), 25 deletions(-) create mode 100644 models/changekeys/all.go create mode 100644 models/changekeys/listed.go create mode 100644 models/changekeys/many.go create mode 100644 models/changekeys/many_test.go create mode 100644 models/changekeys/one.go create mode 100644 models/changekeys/one_test.go diff --git a/graph2/queries/channel.go b/graph2/queries/channel.go index 58e46f2..79b5049 100644 --- a/graph2/queries/channel.go +++ b/graph2/queries/channel.go @@ -7,6 +7,7 @@ import ( "git.aiterp.net/rpdata/api/graph2/input" "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/models/channels" ) @@ -51,7 +52,7 @@ func (r *mutationResolver) AddChannel(ctx context.Context, input input.ChannelAd return models.Channel{}, errors.New("Failed to add channel: " + err.Error()) } - go changes.Submit("Channel", "add", token.UserID, channel) + go changes.Submit("Channel", "add", token.UserID, true, changekeys.Listed(channel), channel) return channel, nil } @@ -72,7 +73,7 @@ func (r *mutationResolver) EditChannel(ctx context.Context, input input.ChannelE return models.Channel{}, errors.New("Failed to edit channel: " + err.Error()) } - go changes.Submit("Channel", "edit", token.UserID, channel) + go changes.Submit("Channel", "edit", token.UserID, true, changekeys.Listed(channel), channel) return channel, nil } diff --git a/graph2/queries/character.go b/graph2/queries/character.go index 2e9cc6c..fb48aa3 100644 --- a/graph2/queries/character.go +++ b/graph2/queries/character.go @@ -8,6 +8,7 @@ import ( "git.aiterp.net/rpdata/api/graph2/input" "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/models/characters" "git.aiterp.net/rpdata/api/models/logs" @@ -68,7 +69,7 @@ func (r *mutationResolver) AddCharacter(ctx context.Context, input input.Charact return models.Character{}, errors.New("Adding character failed: " + err.Error()) } - go changes.Submit("Character", "add", token.UserID, character) + go changes.Submit("Character", "add", token.UserID, true, changekeys.Listed(character), character) return character, nil } @@ -96,7 +97,7 @@ func (r *mutationResolver) AddCharacterNick(ctx context.Context, input input.Cha } go logs.ScheduleFullUpdate() - go changes.Submit("Character", "edit", token.UserID, character) + go changes.Submit("Character", "edit", token.UserID, true, changekeys.Listed(character), character) return character, nil } @@ -118,7 +119,7 @@ func (r *mutationResolver) RemoveCharacterNick(ctx context.Context, input input. } go logs.ScheduleFullUpdate() - go changes.Submit("Character", "edit", token.UserID, character) + go changes.Submit("Character", "edit", token.UserID, true, changekeys.Listed(character), character) return character, nil } @@ -146,7 +147,7 @@ func (r *mutationResolver) EditCharacter(ctx context.Context, input input.Charac return models.Character{}, errors.New("Failed to edit character: " + err.Error()) } - go changes.Submit("Character", "edit", token.UserID, character) + go changes.Submit("Character", "edit", token.UserID, true, changekeys.Listed(character), character) return character, nil } @@ -167,7 +168,7 @@ func (r *mutationResolver) RemoveCharacter(ctx context.Context, input input.Char return models.Character{}, errors.New("Failed to remove character: " + err.Error()) } - go changes.Submit("Character", "remove", token.UserID, character) + go changes.Submit("Character", "remove", token.UserID, true, changekeys.Listed(character), character) return character, nil } diff --git a/graph2/queries/log.go b/graph2/queries/log.go index 9fdddf0..094da72 100644 --- a/graph2/queries/log.go +++ b/graph2/queries/log.go @@ -9,6 +9,7 @@ import ( "git.aiterp.net/rpdata/api/internal/auth" "git.aiterp.net/rpdata/api/internal/loader" "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/models/logs" "github.com/99designs/gqlgen/graphql" @@ -77,7 +78,7 @@ func (r *mutationResolver) AddLog(ctx context.Context, input input.LogAddInput) return models.Log{}, errors.New("Failed to create log: " + err.Error()) } - go changes.Submit("Log", "add", token.UserID, log) + go changes.Submit("Log", "add", token.UserID, true, changekeys.Listed(log), log) return log, nil } @@ -98,7 +99,7 @@ func (r *mutationResolver) EditLog(ctx context.Context, input input.LogEditInput return models.Log{}, errors.New("Failed to edit log: " + err.Error()) } - go changes.Submit("Log", "edit", token.UserID, log) + go changes.Submit("Log", "edit", token.UserID, true, changekeys.Listed(log), log) return log, nil } @@ -119,7 +120,7 @@ func (r *mutationResolver) RemoveLog(ctx context.Context, input input.LogRemoveI return models.Log{}, errors.New("Failed to remove log: " + err.Error()) } - go changes.Submit("Log", "remove", token.UserID, log) + go changes.Submit("Log", "remove", token.UserID, true, changekeys.Listed(log), log) return log, nil } diff --git a/graph2/queries/post.go b/graph2/queries/post.go index 3ed64cc..ead311e 100644 --- a/graph2/queries/post.go +++ b/graph2/queries/post.go @@ -4,6 +4,7 @@ import ( "context" "errors" + "git.aiterp.net/rpdata/api/models/changekeys" "git.aiterp.net/rpdata/api/models/changes" "git.aiterp.net/rpdata/api/models/logs" @@ -59,7 +60,7 @@ func (r *mutationResolver) AddPost(ctx context.Context, input input.PostAddInput } go logs.UpdateCharacters(log) - go changes.Submit("Post", "add", token.UserID, post) + go changes.Submit("Post", "add", token.UserID, true, changekeys.Many(log, post), post) return post, nil } @@ -91,7 +92,14 @@ func (r *mutationResolver) EditPost(ctx context.Context, input input.PostEditInp return models.Post{}, errors.New("Adding post failed: " + err.Error()) } - go changes.Submit("Post", "edit", token.UserID, post) + go func() { + log, err := logs.FindID(post.LogID) + if err != nil { + log = models.Log{ID: post.LogID} + } + + changes.Submit("Post", "edit", token.UserID, true, changekeys.Many(log, post), post) + }() return post, nil } @@ -112,11 +120,14 @@ func (r *mutationResolver) MovePost(ctx context.Context, input input.PostMoveInp return nil, errors.New("Moving posts failed: " + err.Error()) } - posts2 := make([]interface{}, len(posts)) - for i := range posts { - posts2[i] = posts[i] - } - go changes.Submit("Post", "move", token.UserID, posts2...) + go func() { + log, err := logs.FindID(post.LogID) + if err != nil { + log = models.Log{ID: post.LogID} + } + + changes.Submit("Post", "move", token.UserID, true, changekeys.Many(log, posts), post) + }() return posts, nil } @@ -144,8 +155,8 @@ func (r *mutationResolver) RemovePost(ctx context.Context, input input.PostRemov } logs.UpdateCharacters(log) + changes.Submit("Post", "remove", token.UserID, true, changekeys.Many(log, post), post) }() - go changes.Submit("Post", "remove", token.UserID, post) return post, nil } diff --git a/models/change.go b/models/change.go index f920814..bf6baeb 100644 --- a/models/change.go +++ b/models/change.go @@ -4,17 +4,25 @@ import "time" // Change represents a change in the rpdata history through the API. type Change struct { - ID string `bson:"_id"` - Model string `bson:"model"` - Op string `bson:"op"` - Author string `bson:"author"` - Date time.Time `bson:"date"` + ID string `bson:"_id"` + Model string `bson:"model"` + Op string `bson:"op"` + Author string `bson:"author"` + Listed bool `bson:"listed"` + Keys []ChangeKey `bson:"keys"` + Date time.Time `bson:"date"` Logs []Log `bson:"logs"` Characters []Character `bson:"characters"` Posts []Post `bson:"posts"` } +// ChangeKey is a key for a change that can be used when subscribing to them. +type ChangeKey struct { + Model string `bson:"model"` + ID string `bson:"id"` +} + // Data makes a combined, mixed array of all the models stored in this change. func (change *Change) Data() []interface{} { data := make([]interface{}, 0, len(change.Logs)+len(change.Characters)+len(change.Posts)) diff --git a/models/changekeys/all.go b/models/changekeys/all.go new file mode 100644 index 0000000..d84ce88 --- /dev/null +++ b/models/changekeys/all.go @@ -0,0 +1,8 @@ +package changekeys + +import "git.aiterp.net/rpdata/api/models" + +// All makes a changelog that's (model, "*"). It's a helper to standardize it. +func All(model string) models.ChangeKey { + return models.ChangeKey{Model: model, ID: "*"} +} diff --git a/models/changekeys/listed.go b/models/changekeys/listed.go new file mode 100644 index 0000000..a0c4ee8 --- /dev/null +++ b/models/changekeys/listed.go @@ -0,0 +1,10 @@ +package changekeys + +import "git.aiterp.net/rpdata/api/models" + +// Listed is a helper for cases like []models.ChangeKey{changekeys.All("Logs"), changekeys.One(log)} +func Listed(object interface{}) []models.ChangeKey { + key := One(object) + + return []models.ChangeKey{key, All(key.Model)} +} diff --git a/models/changekeys/many.go b/models/changekeys/many.go new file mode 100644 index 0000000..bf1396d --- /dev/null +++ b/models/changekeys/many.go @@ -0,0 +1,27 @@ +package changekeys + +import ( + "reflect" + + "git.aiterp.net/rpdata/api/models" +) + +// Many returns a key set matching the input objects, but without the asterisk key. You may pass +// a slice as an argument, for example (log, posts) and it will be flattened +func Many(objects ...interface{}) []models.ChangeKey { + keys := make([]models.ChangeKey, 0, len(objects)) + for _, object := range objects { + if v := reflect.ValueOf(object); v.Kind() == reflect.Slice { + elems := make([]interface{}, 0, v.Len()) + for i := 0; i < v.Len(); i++ { + elems = append(elems, v.Index(i).Interface()) + } + + keys = append(keys, Many(elems...)...) + } else { + keys = append(keys, One(object)) + } + } + + return keys +} diff --git a/models/changekeys/many_test.go b/models/changekeys/many_test.go new file mode 100644 index 0000000..2833b70 --- /dev/null +++ b/models/changekeys/many_test.go @@ -0,0 +1,52 @@ +package changekeys_test + +import ( + "testing" + + "git.aiterp.net/rpdata/api/models/changekeys" + + "git.aiterp.net/rpdata/api/models" +) + +func TestMany(t *testing.T) { + data := []interface{}{ + models.Log{ID: "Stuff"}, + []interface{}{ + models.Post{ID: "P1"}, + models.Post{ID: "P2"}, + models.Post{ID: "P3"}, + []interface{}{ + models.Post{ID: "P4"}, + models.Character{ID: "C17"}, + models.Channel{Name: "#Stuff"}, + }, + }, + models.Post{ID: "P5"}, + } + + expectations := []models.ChangeKey{ + {Model: "Log", ID: "Stuff"}, + {Model: "Post", ID: "P1"}, + {Model: "Post", ID: "P2"}, + {Model: "Post", ID: "P3"}, + {Model: "Post", ID: "P4"}, + {Model: "Character", ID: "C17"}, + {Model: "Channel", ID: "#Stuff"}, + {Model: "Post", ID: "P5"}, + } + + results := changekeys.Many(data...) + + if len(results) != len(expectations) { + t.Fatal("Incorrect result length") + } + + for i := range expectations { + result := results[i] + expectation := expectations[i] + + if result.ID != expectation.ID || result.Model != expectation.Model { + t.Errorf("Incorrect: actual(%#+v, %#+v) != expected(%#+v, %#+v)", result.Model, result.ID, expectation.Model, expectation.ID) + } + } +} diff --git a/models/changekeys/one.go b/models/changekeys/one.go new file mode 100644 index 0000000..9316d2f --- /dev/null +++ b/models/changekeys/one.go @@ -0,0 +1,29 @@ +package changekeys + +import ( + "reflect" + + "git.aiterp.net/rpdata/api/models" +) + +// One makes a ChangeKey for a model, or panics if it's not supported. +func One(object interface{}) models.ChangeKey { + model := "" + if t := reflect.TypeOf(object); t.Kind() == reflect.Ptr { + model = t.Elem().Name() + } else { + model = t.Name() + } + + id := "" + v := reflect.ValueOf(object) + if f := v.FieldByName("ID"); f.Kind() == reflect.String { + id = f.String() + } else if f = v.FieldByName("Name"); f.Kind() == reflect.String { + id = f.String() + } else { + panic("Unsupported model") + } + + return models.ChangeKey{Model: model, ID: id} +} diff --git a/models/changekeys/one_test.go b/models/changekeys/one_test.go new file mode 100644 index 0000000..d4d6d80 --- /dev/null +++ b/models/changekeys/one_test.go @@ -0,0 +1,42 @@ +package changekeys_test + +import ( + "testing" + + "git.aiterp.net/rpdata/api/models" + "git.aiterp.net/rpdata/api/models/changekeys" +) + +func TestOne(t *testing.T) { + table := []struct { + Label string + Input interface{} + Output models.ChangeKey + }{ + { + "Character_C17", + models.Character{ID: "C17"}, + models.ChangeKey{Model: "Character", ID: "C17"}, + }, + { + "Channel_#Miner'sRespite", + models.Channel{Name: "#Miner'sRespite"}, + models.ChangeKey{Model: "Channel", ID: "#Miner'sRespite"}, + }, + { + "Log_2018-10-23_210303325_RedrockAgency", + models.Log{ID: "2018-10-23_210303325_RedrockAgency", ShortID: "L807"}, + models.ChangeKey{Model: "Log", ID: "2018-10-23_210303325_RedrockAgency"}, + }, + } + + for _, row := range table { + t.Run(row.Label, func(t *testing.T) { + key := changekeys.One(row.Input) + + if key.ID != row.Output.ID || key.Model != row.Output.Model { + t.Errorf("Incorrect: actual(%#+v, %#+v) != expected(%#+v, %#+v)", key.Model, key.ID, row.Output.Model, row.Output.ID) + } + }) + } +} diff --git a/models/changes/db.go b/models/changes/db.go index beb46b1..00e9893 100644 --- a/models/changes/db.go +++ b/models/changes/db.go @@ -18,6 +18,7 @@ func init() { collection.EnsureIndexKey("date") collection.EnsureIndexKey("author") + collection.EnsureIndexKey("keys") err := collection.EnsureIndex(mgo.Index{ Name: "expiry", diff --git a/models/changes/submit.go b/models/changes/submit.go index b1ed757..e44f875 100644 --- a/models/changes/submit.go +++ b/models/changes/submit.go @@ -8,8 +8,8 @@ import ( "git.aiterp.net/rpdata/api/models" ) -// Submit a change to the database -func Submit(model, op, author string, objects ...interface{}) (models.Change, error) { +// Submit a change to the database. The objects may be any supported model, or arrays. +func Submit(model, op, author string, listed bool, keys []models.ChangeKey, objects ...interface{}) (models.Change, error) { submitMutex.Lock() defer submitMutex.Unlock() @@ -24,16 +24,24 @@ func Submit(model, op, author string, objects ...interface{}) (models.Change, er Date: time.Now(), Op: op, Author: author, + Keys: keys, + Listed: listed, } for _, object := range objects { switch object := object.(type) { case models.Log: change.Logs = append(change.Logs, object) + case []models.Log: + change.Logs = append(change.Logs, object...) case models.Character: change.Characters = append(change.Characters, object) + case []models.Character: + change.Characters = append(change.Characters, object...) case models.Post: change.Posts = append(change.Posts, object) + case []models.Post: + change.Posts = append(change.Posts, object...) } }