From ffec0088202569481202ecade07fbcd8fb056038 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Wed, 31 Oct 2018 21:38:45 +0100 Subject: [PATCH] graph2: Added change type and query. --- graph2/gqlgen.yml | 10 +++++ graph2/graph.go | 4 ++ graph2/queries/changes.go | 14 ++++++ graph2/schema/root.gql | 4 ++ graph2/schema/types/Change.gql | 81 ++++++++++++++++++++++++++++++++++ graph2/types/change.go | 24 ++++++++++ models/change-model.go | 59 +++++++++++++++++++++++++ models/change.go | 10 ++--- models/changekeys/all.go | 2 +- models/changekeys/listed.go | 2 +- models/changekeys/one.go | 2 +- models/changes/db.go | 9 ++++ models/changes/list.go | 42 ++++++++++++++++++ models/changes/submit.go | 24 +++++++++- 14 files changed, 278 insertions(+), 9 deletions(-) create mode 100644 graph2/queries/changes.go create mode 100644 graph2/schema/types/Change.gql create mode 100644 graph2/types/change.go create mode 100644 models/change-model.go create mode 100644 models/changes/list.go diff --git a/graph2/gqlgen.yml b/graph2/gqlgen.yml index c94e1af..820213e 100644 --- a/graph2/gqlgen.yml +++ b/graph2/gqlgen.yml @@ -49,6 +49,16 @@ models: model: git.aiterp.net/rpdata/api/models.File FilesFilter: model: git.aiterp.net/rpdata/api/models/files.Filter + Change: + model: git.aiterp.net/rpdata/api/models.Change + ChangeModel: + model: git.aiterp.net/rpdata/api/models.ChangeModel + ChangeKey: + model: git.aiterp.net/rpdata/api/models.ChangeKey + 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 Token: model: git.aiterp.net/rpdata/api/models.Token User: diff --git a/graph2/graph.go b/graph2/graph.go index 4e1623d..f4ced67 100644 --- a/graph2/graph.go +++ b/graph2/graph.go @@ -42,6 +42,10 @@ func (r *rootResolver) File() FileResolver { return &types.FileResolver } +func (r *rootResolver) Change() ChangeResolver { + return &types.ChangeResolver +} + func (r *rootResolver) Token() TokenResolver { return &types.TokenResolver } diff --git a/graph2/queries/changes.go b/graph2/queries/changes.go new file mode 100644 index 0000000..51b089c --- /dev/null +++ b/graph2/queries/changes.go @@ -0,0 +1,14 @@ +package queries + +import ( + "context" + + "git.aiterp.net/rpdata/api/models" + "git.aiterp.net/rpdata/api/models/changes" +) + +/// Queries + +func (r *resolver) Changes(ctx context.Context, filter *changes.Filter) ([]models.Change, error) { + return changes.List(filter) +} diff --git a/graph2/schema/root.gql b/graph2/schema/root.gql index e08c4c6..51297bd 100644 --- a/graph2/schema/root.gql +++ b/graph2/schema/root.gql @@ -54,6 +54,10 @@ type Query { files(filter: FilesFilter): [File!]! + # Find changes + changes(filter: ChangesFilter): [Change!]! + + # Get information about the token, useful for debugging. token: Token! } diff --git a/graph2/schema/types/Change.gql b/graph2/schema/types/Change.gql new file mode 100644 index 0000000..a8874d7 --- /dev/null +++ b/graph2/schema/types/Change.gql @@ -0,0 +1,81 @@ +""" +A change represents a a change in the history. +""" +type Change { + "A unique ID of the change." + id: String! + + "The model of the changed object." + model: ChangeModel! + + "The operation performed." + op: String! + + "The author that performed the change." + author: String! + + "Whether the change will be listed without a matching key." + listed: Boolean! + + "The keys indexing this change." + keys: [ChangeKey!]! + + "The date when the change happened." + date: Date + + "The changed objects." + objects: [ChangeObject!]! +} + +union ChangeObject = Character | Channel | Log | Post | Story | Tag | Chapter + +""" +A change key is a key associated with a change, used for filtering +and subscription. A log post edit has keys for both the log and post, +making it suitable to use it to patch an active log file. +""" +type ChangeKey { + "The model the key is for." + model: ChangeModel! + + "The model's id, or * if it's a key indicating a model list should be updated." + id: String! +} + +""" +See ChangeKey +""" +input ChangeKeyInput { + "The model the key is for." + model: ChangeModel! + + "The model's id, or * if it's a key indicating a model list should be updated." + id: String! +} + +""" +A model related to the change. +""" +enum ChangeModel { + Character + Channel + Log + Post + Story + Tag + Chapter +} + +""" +Optional filter for the changes query. +""" +input ChangesFilter { + "The keys to query for." + keys: [ChangeKeyInput!] + + "Only show changes more recent than this date." + earliestDate: Date + + "Limit the amount of results." + limit: Int +} \ No newline at end of file diff --git a/graph2/types/change.go b/graph2/types/change.go new file mode 100644 index 0000000..b1f9a48 --- /dev/null +++ b/graph2/types/change.go @@ -0,0 +1,24 @@ +package types + +import ( + "context" + + "git.aiterp.net/rpdata/api/graph2/input" + "git.aiterp.net/rpdata/api/models" +) + +type changeResolver struct{} + +func (r *changeResolver) Objects(ctx context.Context, obj *models.Change) ([]input.ChangeObject, error) { + objects := obj.Objects() + + results := make([]input.ChangeObject, len(objects)) + for i := range objects { + results[i] = objects[i] + } + + return results, nil +} + +// ChangeResolver is a resolver +var ChangeResolver changeResolver diff --git a/models/change-model.go b/models/change-model.go new file mode 100644 index 0000000..75395f2 --- /dev/null +++ b/models/change-model.go @@ -0,0 +1,59 @@ +package models + +import ( + "fmt" + "io" + "strconv" +) + +// ChangeModel describes a model related to the change. +type ChangeModel string + +const ( + // ChangeModelCharacter is a value of ChangeModel + ChangeModelCharacter ChangeModel = "Character" + // ChangeModelChannel is a value of ChangeModel + ChangeModelChannel ChangeModel = "Channel" + // ChangeModelLog is a value of ChangeModel + ChangeModelLog ChangeModel = "Log" + // ChangeModelPost is a value of ChangeModel + ChangeModelPost ChangeModel = "Post" + // ChangeModelStory is a value of ChangeModel + ChangeModelStory ChangeModel = "Story" + // ChangeModelTag is a value of ChangeModel + ChangeModelTag ChangeModel = "Tag" + // ChangeModelChapter is a value of ChangeModel + ChangeModelChapter ChangeModel = "Chapter" +) + +// IsValid returns true if the underlying string is one of the correct values. +func (e ChangeModel) IsValid() bool { + switch e { + case ChangeModelCharacter, ChangeModelChannel, ChangeModelLog, ChangeModelPost, ChangeModelStory, ChangeModelTag, ChangeModelChapter: + return true + } + return false +} + +func (e ChangeModel) String() string { + return string(e) +} + +// UnmarshalGQL unmarshals the underlying graphql value. +func (e *ChangeModel) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = ChangeModel(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid ChangeModel", str) + } + return nil +} + +// MarshalGQL marshals the underlying graphql value. +func (e ChangeModel) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} diff --git a/models/change.go b/models/change.go index 252984b..be34c72 100644 --- a/models/change.go +++ b/models/change.go @@ -5,7 +5,7 @@ import "time" // Change represents a change in the rpdata history through the API. type Change struct { ID string `bson:"_id"` - Model string `bson:"model"` + Model ChangeModel `bson:"model"` Op string `bson:"op"` Author string `bson:"author"` Listed bool `bson:"listed"` @@ -23,12 +23,12 @@ type Change struct { // 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"` + Model ChangeModel `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{} { +// 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)) for _, log := range change.Logs { diff --git a/models/changekeys/all.go b/models/changekeys/all.go index d84ce88..16afb4a 100644 --- a/models/changekeys/all.go +++ b/models/changekeys/all.go @@ -4,5 +4,5 @@ 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: "*"} + return models.ChangeKey{Model: models.ChangeModel(model), ID: "*"} } diff --git a/models/changekeys/listed.go b/models/changekeys/listed.go index c39b423..918a963 100644 --- a/models/changekeys/listed.go +++ b/models/changekeys/listed.go @@ -9,5 +9,5 @@ func Listed(objects ...interface{}) []models.ChangeKey { return nil } - return append(keys, All(keys[0].Model)) + return append(keys, All(keys[0].Model.String())) } diff --git a/models/changekeys/one.go b/models/changekeys/one.go index 462530f..deb674b 100644 --- a/models/changekeys/one.go +++ b/models/changekeys/one.go @@ -34,5 +34,5 @@ func One(object interface{}) models.ChangeKey { panic("Unsupported model") } - return models.ChangeKey{Model: model, ID: id} + return models.ChangeKey{Model: models.ChangeModel(model), ID: id} } diff --git a/models/changes/db.go b/models/changes/db.go index 00e9893..48d66ba 100644 --- a/models/changes/db.go +++ b/models/changes/db.go @@ -6,12 +6,21 @@ import ( "time" "git.aiterp.net/rpdata/api/internal/store" + "git.aiterp.net/rpdata/api/models" "github.com/globalsign/mgo" + "github.com/globalsign/mgo/bson" ) var collection *mgo.Collection var submitMutex sync.Mutex +func list(query bson.M, limit int) ([]models.Change, error) { + changes := make([]models.Change, 0, 64) + err := collection.Find(query).Limit(limit).Sort("date").All(&changes) + + return changes, err +} + func init() { store.HandleInit(func(db *mgo.Database) { collection = db.C("common.changes") diff --git a/models/changes/list.go b/models/changes/list.go new file mode 100644 index 0000000..ebcca3e --- /dev/null +++ b/models/changes/list.go @@ -0,0 +1,42 @@ +package changes + +import ( + "time" + + "git.aiterp.net/rpdata/api/models" + "github.com/globalsign/mgo/bson" +) + +// Filter is a filter for changes.List. +type Filter struct { + Keys []models.ChangeKey + EarliestDate *time.Time + Limit *int +} + +// List lists changes. +func List(filter *Filter) ([]models.Change, error) { + query := bson.M{} + limit := 0 + + if filter != nil { + if filter.Limit != nil { + limit = *filter.Limit + } + + if filter.Keys != nil { + query["keys"] = bson.M{"$in": filter.Keys} + } else { + query["listed"] = true + } + + if filter.EarliestDate != nil { + query["date"] = bson.M{"$gt": *filter.EarliestDate} + } + } else { + query["listed"] = true + limit = 256 + } + + return list(query, limit) +} diff --git a/models/changes/submit.go b/models/changes/submit.go index 05daac2..cf628b2 100644 --- a/models/changes/submit.go +++ b/models/changes/submit.go @@ -18,9 +18,31 @@ func Submit(model, op, author string, listed bool, keys []models.ChangeKey, obje return models.Change{}, err } + if !models.ChangeModel(model).IsValid() { + panic("Invalid model") + } + + // Silently discard * keys on unlisted changes. + if !listed { + keysCopy := make([]models.ChangeKey, len(keys)) + copy(keysCopy, keys) + keys = keysCopy + + deletes := make([]int, 0, 1) + for i, key := range keys { + if key.ID == "*" { + deletes = append(deletes, i-len(deletes)) + } + } + + for _, index := range deletes { + keys = append(keys[:index], keys[index+1:]...) + } + } + change := models.Change{ ID: "Change_" + strconv.Itoa(id), - Model: model, + Model: models.ChangeModel(model), Date: time.Now(), Op: op, Author: author,