From 9978d7380b3c93e14101dfb0ab6d5a1dc377665d Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Thu, 13 Sep 2018 21:13:05 +0200 Subject: [PATCH] graph, rpdata-server: Started on new GraphQL implementation. --- .gitignore | 6 ++- Gopkg.lock | 90 ++++++++++++++++++++++++++++--- Gopkg.toml | 4 +- cmd/rpdata-server/main.go | 46 ++++++++++++++++ graph2/combine.sh | 4 ++ graph2/gqlgen.yml | 19 +++++++ graph2/graph.go | 21 ++++++++ graph2/queries/character.go | 22 ++++++++ graph2/queries/resolver.go | 6 +++ graph2/queries/tags.go | 11 ++++ graph2/schema/root.gql | 16 ++++++ graph2/schema/types/Character.gql | 87 ++++++++++++++++++++++++++++++ graph2/schema/types/Tag.gql | 37 +++++++++++++ model/character/character.go | 63 +++++++++++++++++++++- model/story/tag-kind.go | 58 ++++++++++++++++++++ model/story/tag.go | 6 +-- 16 files changed, 482 insertions(+), 14 deletions(-) create mode 100644 cmd/rpdata-server/main.go create mode 100755 graph2/combine.sh create mode 100644 graph2/gqlgen.yml create mode 100644 graph2/graph.go create mode 100644 graph2/queries/character.go create mode 100644 graph2/queries/resolver.go create mode 100644 graph2/queries/tags.go create mode 100644 graph2/schema/root.gql create mode 100644 graph2/schema/types/Character.gql create mode 100644 graph2/schema/types/Tag.gql create mode 100644 model/story/tag-kind.go diff --git a/.gitignore b/.gitignore index cde4f6a..6673993 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,8 @@ debug build rpdata-graphiql rpdata-* -.vscode \ No newline at end of file +!rpdata-*/ +.vscode + +generated.gql +generated.go \ No newline at end of file diff --git a/Gopkg.lock b/Gopkg.lock index 4134579..b8c507f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,10 +2,32 @@ [[projects]] - branch = "master" - name = "git.aiterp.net/aiterp/wikiauth" + name = "github.com/99designs/gqlgen" + packages = [ + ".", + "cmd", + "codegen", + "codegen/templates", + "complexity", + "graphql", + "graphql/introspection", + "handler", + "internal/gopath" + ] + revision = "636435b68700211441303f1a5ed92f3768ba5774" + version = "v0.5.1" + +[[projects]] + name = "github.com/agnivade/levenshtein" + packages = ["."] + revision = "3d21ba515fe27b856f230847e856431ae1724adc" + version = "v1.0.0" + +[[projects]] + name = "github.com/dgrijalva/jwt-go" packages = ["."] - revision = "33860de804ddcec4fc17998745233b4ec477e7c2" + revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e" + version = "v3.2.0" [[projects]] branch = "master" @@ -37,6 +59,12 @@ revision = "d523deb1b23d913de5bdada721a6071e71283618" version = "v1.4.0" +[[projects]] + name = "github.com/gorilla/websocket" + packages = ["."] + revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d" + version = "v1.4.0" + [[projects]] name = "github.com/graph-gophers/dataloader" packages = ["."] @@ -64,6 +92,15 @@ ] revision = "9ebf33af539ab8cb832c7107bc0a978ca8dbc0de" +[[projects]] + name = "github.com/hashicorp/golang-lru" + packages = [ + ".", + "simplelru" + ] + revision = "20f1fb78b0740ba8c3cb143a61e86ba5c8669768" + version = "v0.5.0" + [[projects]] branch = "master" name = "github.com/jmoiron/sqlx" @@ -103,10 +140,10 @@ version = "v1.0.2" [[projects]] - name = "github.com/sadbox/mediawiki" + name = "github.com/pkg/errors" packages = ["."] - revision = "39fea8a1336076a961a300d1d95765dcd17e8a3c" - version = "v0.1" + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" [[projects]] name = "github.com/sirupsen/logrus" @@ -114,6 +151,26 @@ revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc" version = "v1.0.5" +[[projects]] + name = "github.com/urfave/cli" + packages = ["."] + revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1" + version = "v1.20.0" + +[[projects]] + branch = "master" + name = "github.com/vektah/gqlparser" + packages = [ + ".", + "ast", + "gqlerror", + "lexer", + "parser", + "validator", + "validator/rules" + ] + revision = "14e83ae06ec152e6d0afb9766a00e0c0918aa8fc" + [[projects]] branch = "master" name = "golang.org/x/crypto" @@ -164,15 +221,34 @@ revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" version = "v0.3.0" +[[projects]] + branch = "master" + name = "golang.org/x/tools" + packages = [ + "go/ast/astutil", + "go/buildutil", + "go/internal/cgo", + "go/loader", + "imports", + "internal/fastwalk" + ] + revision = "677d2ff680c188ddb7dcd2bfa6bc7d3f2f2f75b2" + [[projects]] name = "google.golang.org/appengine" packages = ["cloudsql"] revision = "b1f26356af11148e710935ed1ac8a7f5702c7612" version = "v1.1.0" +[[projects]] + name = "gopkg.in/yaml.v2" + packages = ["."] + revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" + version = "v2.2.1" + [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "16fd84401bc5796ffeaa895f31d099e1a23b363b15b87386877de0829ac9575b" + inputs-digest = "5395eefae297e64dc7575e2e7a24cd739dabbeaae561de7d876249f57a23619a" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index e1b8166..da8e761 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -24,6 +24,8 @@ # go-tests = true # unused-packages = true +required = [ "github.com/99designs/gqlgen" ] + [[constraint]] branch = "master" name = "github.com/globalsign/mgo" @@ -38,4 +40,4 @@ [[constraint]] branch = "master" - name = "github.com/graph-gophers/graphql-go" + name = "github.com/graph-gophers/graphql-go" \ No newline at end of file diff --git a/cmd/rpdata-server/main.go b/cmd/rpdata-server/main.go new file mode 100644 index 0000000..7b7a5c2 --- /dev/null +++ b/cmd/rpdata-server/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "runtime/debug" + + "git.aiterp.net/rpdata/api/graph2" + "git.aiterp.net/rpdata/api/internal/store" + logModel "git.aiterp.net/rpdata/api/model/log" + "github.com/99designs/gqlgen/handler" +) + +func main() { + err := store.Init() + if err != nil { + log.Fatalln("Failed to init store:", err) + } + + http.Handle("/", handler.Playground("RPData API", "/query")) + http.Handle("/query", handler.GraphQL( + graph2.New(), + handler.RecoverFunc(func(ctx context.Context, err interface{}) error { + // send this panic somewhere + log.Println(err) + log.Println(string(debug.Stack())) + + return fmt.Errorf("shit") + }), + )) + + go updateCharacters() + + log.Fatal(http.ListenAndServe(":8081", nil)) +} + +func updateCharacters() { + n, err := logModel.UpdateAllCharacters() + if err != nil { + log.Println("Charcter updated stopped:", err) + } + + log.Println("Updated characters on", n, "logs") +} diff --git a/graph2/combine.sh b/graph2/combine.sh new file mode 100755 index 0000000..2343b5f --- /dev/null +++ b/graph2/combine.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +echo "# Generated by stiching together the files under schema/ – DO NOT EDIT" >generated.gql +cat schema/root.gql schema/**/*.gql >>generated.gql \ No newline at end of file diff --git a/graph2/gqlgen.yml b/graph2/gqlgen.yml new file mode 100644 index 0000000..1ebbd3d --- /dev/null +++ b/graph2/gqlgen.yml @@ -0,0 +1,19 @@ +schema: generated.gql + +exec: + filename: generated.go + package: graph2 + +model: + filename: input/generated.go + package: input + +models: + Tag: + model: git.aiterp.net/rpdata/api/model/story.Tag + TagKind: + model: git.aiterp.net/rpdata/api/model/story.TagKind + Character: + model: git.aiterp.net/rpdata/api/model/character.Character + CharactersFilter: + model: git.aiterp.net/rpdata/api/model/character.Filter diff --git a/graph2/graph.go b/graph2/graph.go new file mode 100644 index 0000000..9f8a4bb --- /dev/null +++ b/graph2/graph.go @@ -0,0 +1,21 @@ +package graph2 + +import ( + "git.aiterp.net/rpdata/api/graph2/queries" + graphql "github.com/99designs/gqlgen/graphql" +) + +//go:generate ./combine.sh +//go:generate gorunpkg github.com/99designs/gqlgen -v + +func New() graphql.ExecutableSchema { + return NewExecutableSchema(Config{ + Resolvers: &rootResolver{}, + }) +} + +type rootResolver struct{} + +func (r *rootResolver) Query() QueryResolver { + return &queries.Resolver +} diff --git a/graph2/queries/character.go b/graph2/queries/character.go new file mode 100644 index 0000000..6ef64c1 --- /dev/null +++ b/graph2/queries/character.go @@ -0,0 +1,22 @@ +package queries + +import ( + "context" + "errors" + + "git.aiterp.net/rpdata/api/model/character" +) + +func (r *resolver) Character(ctx context.Context, id *string, nick *string) (character.Character, error) { + if id != nil { + return character.FindID(*id) + } else if nick != nil { + return character.FindNick(*nick) + } else { + return character.Character{}, errors.New("You must specify either an ID or a nick") + } +} + +func (r *resolver) Characters(ctx context.Context, filter *character.Filter) ([]character.Character, error) { + return character.List(filter) +} diff --git a/graph2/queries/resolver.go b/graph2/queries/resolver.go new file mode 100644 index 0000000..47e66a9 --- /dev/null +++ b/graph2/queries/resolver.go @@ -0,0 +1,6 @@ +package queries + +type resolver struct{} + +// Resolver has all the queries +var Resolver resolver diff --git a/graph2/queries/tags.go b/graph2/queries/tags.go new file mode 100644 index 0000000..a8d7fd7 --- /dev/null +++ b/graph2/queries/tags.go @@ -0,0 +1,11 @@ +package queries + +import ( + "context" + + "git.aiterp.net/rpdata/api/model/story" +) + +func (r *resolver) Tags(ctx context.Context) ([]story.Tag, error) { + return story.ListTags() +} diff --git a/graph2/schema/root.gql b/graph2/schema/root.gql new file mode 100644 index 0000000..a45ba5b --- /dev/null +++ b/graph2/schema/root.gql @@ -0,0 +1,16 @@ +schema { + query: Query +} + +type Query { + # Find character by either an ID or a nick. + character(id: String, nick: String): Character! + + # Find characters + characters(filter: CharactersFilter): [Character!]! + + + # Find all distinct tags used in stories + tags: [Tag!]! +} + diff --git a/graph2/schema/types/Character.gql b/graph2/schema/types/Character.gql new file mode 100644 index 0000000..ecef7b2 --- /dev/null +++ b/graph2/schema/types/Character.gql @@ -0,0 +1,87 @@ +# A Character represents an RP character +type Character { + # A unique identifier for the character + id: String! + + # The primary IRC nick belonging to the character + nick: String + + # All IRC nicks associated with this character + nicks: [String!]! + + # The character's author + author: String! + + # The character's name + name: String! + + # The name to display when space is scarce, usually the first/given name + shortName: String! + + # A short description of the character + description: String! +} + +# Filter for characters query +input CharactersFilter { + # Filter by character IDs + ids: [String!] + + # Filter by nicks + nicks: [String!] + + # Filter by names + names: [String!] + + # Filter by author + author: String + + # Filter by text search matching against the character's description + search: String + + # Filter by whether they've been part of a log. + logged: Boolean +} + +# Input for adding characters +input CharacterAddInput { + # The primary IRC nick name to recognize this character by + nick: String! + + # The character's name + name: String! + + # Optioanl shortened name. By default, it uses the first token in the name + shortName: String + + # Description for a character. + description: String + + # Optioanlly, specify another author. This needs special permissions if it's not + # your own username + author: String +} + +# Input for addNick and removeNick mutation +input CharacterNickInput { + # The ID of the character + id: String! + + # The nick to add or remove + nick: String! +} + +input CharacterEditInput { + # The id for the character to edit + id: String! + + # The full name of the character -- not the salarian full name! + name: String + + # The character's short name that is used in compact lists + shortName: String + + # A short description for the character + description: String +} + diff --git a/graph2/schema/types/Tag.gql b/graph2/schema/types/Tag.gql new file mode 100644 index 0000000..139530e --- /dev/null +++ b/graph2/schema/types/Tag.gql @@ -0,0 +1,37 @@ +# A Tag is a means of associating stories that have details in common with one another. +type Tag { + # The tag's kind + kind: TagKind! + + # The tag's name + name: String! +} + +# A Tag is a means of associating stories that have details in common with one another. +input TagInput { + # The tag's kind + kind: TagKind! + + # The tag's name + name: String! +} + +# Allowed values for Tag.kind +enum TagKind { + # An organization is a catch all term for in-universe corporations, teams, groups, cults, forces, etc... + Organization + + # A character tag should have the exact full name of the character. + Character + + # A location is anything from a planet to an establishment. This may overlap with an organization, and if so, both + # kinds of tags should be used. + Location + + # An event is a plot or a part of a plot. + Event + + # None of the above, but it does still tie multiple stories together. The new story/chapter format may obsolete this tag kind. + Series +} + diff --git a/model/character/character.go b/model/character/character.go index 7085d6b..d6cc3c8 100644 --- a/model/character/character.go +++ b/model/character/character.go @@ -26,6 +26,25 @@ type Character struct { Description string `json:"description" bson:"description"` } +// Filter is used to filter the list of characters +type Filter struct { + IDs []string `json:"ids"` + Nicks []string `json:"nicks"` + Names []string `json:"names"` + Author *string `json:"author"` + Search *string `json:"search"` + Logged *bool `json:"logged"` +} + +// Nick gets the character's nick. +func (character *Character) Nick() *string { + if len(character.Nicks[0]) == 0 { + return nil + } + + return &character.Nicks[0] +} + // HasNick returns true if the character has that nick func (character *Character) HasNick(nick string) bool { for i := range character.Nicks { @@ -136,8 +155,48 @@ func FindName(name string) (Character, error) { } // List lists all characters -func List() ([]Character, error) { - return list(bson.M{}) +func List(filter *Filter) ([]Character, error) { + query := bson.M{} + + if filter != nil { + if len(filter.IDs) > 1 { + query["id"] = bson.M{"$in": filter.IDs} + } else if len(filter.IDs) == 1 { + query["id"] = filter.IDs[0] + } + + if len(filter.Nicks) > 1 { + query["nicks"] = bson.M{"$in": filter.Nicks} + } else if len(filter.Nicks) == 1 { + query["nicks"] = filter.Nicks[0] + } + + if len(filter.Names) > 1 { + query["$or"] = bson.M{ + "name": bson.M{"$in": filter.Names}, + "shortName": bson.M{"$in": filter.Names}, + } + } else if len(filter.Names) == 1 { + query["$or"] = bson.M{ + "name": filter.Names[0], + "shortName": filter.Names[0], + } + } + + if filter.Logged != nil { + query["logged"] = *filter.Logged + } + + if filter.Author != nil { + query["author"] = *filter.Author + } + + if filter.Search != nil { + query["$text"] = bson.M{"$search": *filter.Search} + } + } + + return list(query) } // ListAuthor lists all characters by author diff --git a/model/story/tag-kind.go b/model/story/tag-kind.go new file mode 100644 index 0000000..d40211e --- /dev/null +++ b/model/story/tag-kind.go @@ -0,0 +1,58 @@ +package story + +import ( + "fmt" + "io" +) + +// TagKind represents the kind of tags. +type TagKind string + +const ( + // TagKindOrganization is a tag kind, see GraphQL documentation. + TagKindOrganization TagKind = "Organization" + + // TagKindCharacter is a tag kind, see GraphQL documentation. + TagKindCharacter TagKind = "Character" + + // TagKindLocation is a tag kind, see GraphQL documentation. + TagKindLocation TagKind = "Location" + + // TagKindEvent is a tag kind, see GraphQL documentation. + TagKindEvent TagKind = "Event" + + // TagKindSeries is a tag kind, see GraphQL documentation. + TagKindSeries TagKind = "Series" +) + +// IsValid returns true if the TagKind is one of the constants +func (e TagKind) IsValid() bool { + switch e { + case TagKindOrganization, TagKindCharacter, TagKindLocation, TagKindEvent, TagKindSeries: + return true + } + return false +} + +func (e TagKind) String() string { + return string(e) +} + +// UnmarshalGQL unmarshals +func (e *TagKind) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = TagKind(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid TagKind", str) + } + return nil +} + +// MarshalGQL turns it into a JSON string +func (e TagKind) MarshalGQL(w io.Writer) { + fmt.Fprint(w, "\""+e.String(), "\"") +} diff --git a/model/story/tag.go b/model/story/tag.go index c0408ca..ff6c9c6 100644 --- a/model/story/tag.go +++ b/model/story/tag.go @@ -9,8 +9,8 @@ import ( // A Tag associates a story with other content, like other stories, logs and more. type Tag struct { - Kind string `bson:"kind"` - Name string `bson:"name"` + Kind TagKind `bson:"kind"` + Name string `bson:"name"` } // Equal returns true if the tags match one another. @@ -24,7 +24,7 @@ func ListTags() ([]Tag, error) { err := storyCollection.Find(bson.M{"listed": true, "tags": bson.M{"$ne": nil}}).Distinct("tags", &tags) sort.Slice(tags, func(i, j int) bool { - kindCmp := strings.Compare(tags[i].Kind, tags[j].Kind) + kindCmp := strings.Compare(string(tags[i].Kind), string(tags[j].Kind)) if kindCmp != 0 { return kindCmp < 0 }