From 79171bf9a22a3d372999d0d4b85bf6e861861d6d Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sun, 16 Sep 2018 12:55:14 +0200 Subject: [PATCH] graph2: Added Story type and queries. --- graph2/gqlgen.yml | 11 +++ graph2/graph.go | 4 + graph2/queries/story.go | 31 +++++++ graph2/schema/root.gql | 7 ++ graph2/schema/types/Story.gql | 151 ++++++++++++++++++++++++++++++++++ graph2/types/story.go | 27 ++++++ models/stories/db.go | 59 +++++++++++++ models/stories/find.go | 11 +++ models/stories/list.go | 67 +++++++++++++++ models/story-category.go | 43 ++++++++++ models/story.go | 18 ++++ 11 files changed, 429 insertions(+) create mode 100644 graph2/queries/story.go create mode 100644 graph2/schema/types/Story.gql create mode 100644 graph2/types/story.go create mode 100644 models/stories/db.go create mode 100644 models/stories/find.go create mode 100644 models/stories/list.go create mode 100644 models/story-category.go create mode 100644 models/story.go diff --git a/graph2/gqlgen.yml b/graph2/gqlgen.yml index 2c8567e..1780a60 100644 --- a/graph2/gqlgen.yml +++ b/graph2/gqlgen.yml @@ -11,6 +11,8 @@ model: models: Tag: model: git.aiterp.net/rpdata/api/models.Tag + TagInput: #It's the same as tags + model: git.aiterp.net/rpdata/api/models.Tag TagKind: model: git.aiterp.net/rpdata/api/models.TagKind Character: @@ -34,6 +36,15 @@ models: fields: fictionalDate: resolver: true + Story: + model: git.aiterp.net/rpdata/api/models.Story + fields: + fictionalDate: + resolver: true + StoryCategory: + model: git.aiterp.net/rpdata/api/models.StoryCategory + StoriesFilter: + model: git.aiterp.net/rpdata/api/models/stories.Filter File: model: git.aiterp.net/rpdata/api/models.File FilesFilter: diff --git a/graph2/graph.go b/graph2/graph.go index 1bad40b..d0fde54 100644 --- a/graph2/graph.go +++ b/graph2/graph.go @@ -30,6 +30,10 @@ func (r *rootResolver) Chapter() ChapterResolver { return &types.ChapterResolver } +func (r *rootResolver) Story() StoryResolver { + return &types.StoryResolver +} + func (r *rootResolver) File() FileResolver { return &types.FileResolver } diff --git a/graph2/queries/story.go b/graph2/queries/story.go new file mode 100644 index 0000000..a4f3457 --- /dev/null +++ b/graph2/queries/story.go @@ -0,0 +1,31 @@ +package queries + +import ( + "context" + "errors" + + "git.aiterp.net/rpdata/api/internal/auth" + "git.aiterp.net/rpdata/api/models" + "git.aiterp.net/rpdata/api/models/stories" +) + +func (r *resolver) Story(ctx context.Context, id string) (models.Story, error) { + return stories.FindID(id) +} + +func (r *resolver) Stories(ctx context.Context, filter *stories.Filter) ([]models.Story, error) { + if filter != nil { + if filter.Unlisted != nil && *filter.Unlisted == true { + token := auth.TokenFromContext(ctx) + if !token.Authenticated() { + return nil, errors.New("You are not permitted to view unlisted stories") + } + + if !token.Permitted("story.unlisted") { + filter.Author = &token.UserID + } + } + } + + return stories.List(filter) +} diff --git a/graph2/schema/root.gql b/graph2/schema/root.gql index a04b67e..1fbe10e 100644 --- a/graph2/schema/root.gql +++ b/graph2/schema/root.gql @@ -39,6 +39,13 @@ type Query { tags: [Tag!]! + # Find story by ID + story(id: String!): Story! + + # Find stories + stories(filter: StoriesFilter): [Story!]! + + # Find file by ID file(id: String!): File! diff --git a/graph2/schema/types/Story.gql b/graph2/schema/types/Story.gql new file mode 100644 index 0000000..e30ea8b --- /dev/null +++ b/graph2/schema/types/Story.gql @@ -0,0 +1,151 @@ +# A Story is a piece of content that, unlike wiki articles, are not something that should be ordered by time instead of title. It can +# contain multiple chapters by one or more autohrs +type Story { + # The story's ID + id: String! + + # The story's name, which will show up in the lists. + name: String! + + # The story's author + author: String! + + # Whether other users may add/edit chapters to the story. + open: Boolean! + + # Whether users without a direct link can find this story. + listed: Boolean! + + # The category of the story + category: StoryCategory! + + # The tags for this tory + tags: [Tag!]! + + # The chapters of this story + chapters: [Chapter!]! + + # The date the story was created. + createdDate: Date! + + # The date the story is set in. + fictionalDate: Date + + # The date of the last major update to the story. This being bumped means a new chapter has been added. + updatedDate: Date! +} + +# Filter for stories query. +input StoriesFilter { + # What author to query for + author: String + + # What category to query for + category: StoryCategory + + # What tags to query for + tags: [TagInput!] + + # If true, it will only show unlisted page you have access to + unlisted: Boolean + + # Whether the page is open for additions by other users + open: Boolean + + # The earliest fictionalDate + earliestFictionalDate: Date + + # The latest fictionalDate + latestFictionalDate: Date + + # The max amount of stories to get (default: 30) + limit: Int +} + +# Input for the addStory mutation +input StoryAddInput { + # Set the name of the story, which will show up as the title, and as a suggestion for + # the first chapter being added. + name: String! + + # Set the category for the new story. + category: StoryCategory! + + # Add the story under another name. This requires the story.add permission, and should not be + # abused. The action will be logged with the actual creator's user ID. + author: String + + # Allow other users to post chapters to the story. + open: Boolean + + # Allow other users to see this page in any lists or search results. + listed: Boolean + + # Set which tags the story should have. + tags: [TagInput!] + + # Set the fictional date of the story. + fictionalDate: Date +} + +# Input for the editStory mutation +input StoryEditInput { + # What story to edit + id: String! + + # Set the name of the story, which will show up as the title, and as a suggestion for + # the first chapter being added. + name: String + + # Set the category for the new story. + category: StoryCategory + + # Add the story under another name. This requires the story.add permission, and should not be + # abused. The action will be logged with the actual creator's user ID. + author: String + + # Change whether to allow others to add chapters + open: Boolean + + # Change whether to show this story in the list and search results + listed: Boolean + + # Set the fictional date of the story. + fictionalDate: Date +} + +# Input for the addStoryTag mutation +input StoryTagAddInput { + # What story to add the tag to. + id: String! + + # The tag to add. + tag: TagInput! +} + +# Input for the removeStoryTag mutation +input StoryTagRemoveInput { + # What story to remove the tag from. + id: String! + + # The tag to remove. + tag: TagInput! +} + +# Possible values for Story.category +enum StoryCategory { + # General information + Info + + # News stories + News + + # Description and content of a document or item + Document + + # Information about something going on in the background that may or may not inform RP + Background + + # A short story + Story +} \ No newline at end of file diff --git a/graph2/types/story.go b/graph2/types/story.go new file mode 100644 index 0000000..b70d368 --- /dev/null +++ b/graph2/types/story.go @@ -0,0 +1,27 @@ +package types + +import ( + "context" + "time" + + "git.aiterp.net/rpdata/api/models/chapters" + + "git.aiterp.net/rpdata/api/models" +) + +type storyResolver struct{} + +func (r *storyResolver) FictionalDate(ctx context.Context, story *models.Story) (*time.Time, error) { + if story.FictionalDate.IsZero() { + return nil, nil + } + + return &story.FictionalDate, nil +} + +func (r *storyResolver) Chapters(ctx context.Context, story *models.Story) ([]models.Chapter, error) { + return chapters.ListStoryID(story.ID) +} + +// StoryResolver is a resolver +var StoryResolver storyResolver diff --git a/models/stories/db.go b/models/stories/db.go new file mode 100644 index 0000000..1a48556 --- /dev/null +++ b/models/stories/db.go @@ -0,0 +1,59 @@ +package stories + +import ( + "crypto/rand" + "encoding/binary" + "strconv" + + "git.aiterp.net/rpdata/api/internal/store" + "git.aiterp.net/rpdata/api/models" + "github.com/globalsign/mgo" +) + +var collection *mgo.Collection + +func find(query interface{}) (models.Story, error) { + story := models.Story{} + err := collection.Find(query).One(&story) + + return story, err +} + +func list(query interface{}, limit int) ([]models.Story, error) { + stories := make([]models.Story, 0, 64) + err := collection.Find(query).Limit(limit).Sort("-updatedDate").All(&stories) + + return stories, err +} + +// makeStoryID makes a random story ID that's 16 characters long +func makeStoryID() string { + result := "S" + offset := 0 + data := make([]byte, 32) + + rand.Read(data) + for len(result) < 16 { + result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36) + offset += 8 + + if offset >= 32 { + rand.Read(data) + offset = 0 + } + } + + return result[:16] +} + +func init() { + store.HandleInit(func(db *mgo.Database) { + collection = db.C("story.stories") + + collection.EnsureIndexKey("tags") + collection.EnsureIndexKey("author") + collection.EnsureIndexKey("updatedDate") + collection.EnsureIndexKey("fictionalDate") + collection.EnsureIndexKey("listed") + }) +} diff --git a/models/stories/find.go b/models/stories/find.go new file mode 100644 index 0000000..a48e472 --- /dev/null +++ b/models/stories/find.go @@ -0,0 +1,11 @@ +package stories + +import ( + "git.aiterp.net/rpdata/api/models" + "github.com/globalsign/mgo/bson" +) + +// FindID finds a story by ID +func FindID(id string) (models.Story, error) { + return find(bson.M{"_id": id}) +} diff --git a/models/stories/list.go b/models/stories/list.go new file mode 100644 index 0000000..bed4bad --- /dev/null +++ b/models/stories/list.go @@ -0,0 +1,67 @@ +package stories + +import ( + "time" + + "git.aiterp.net/rpdata/api/models" + "github.com/globalsign/mgo/bson" +) + +// Filter for stories.List +type Filter struct { + Author *string + Tags []models.Tag + EarliestFictionalDate time.Time + LatestFictionalDate time.Time + Category *models.StoryCategory + Open *bool + Unlisted *bool + Limit int +} + +// List lists stories by any non-zero criteria passed with it. +func List(filter *Filter) ([]models.Story, error) { + query := bson.M{"listed": true} + limit := 0 + + if filter != nil { + if filter.Author != nil { + query["author"] = *filter.Author + } + + if len(filter.Tags) > 0 { + query["tags"] = bson.M{"$in": filter.Tags} + } + + if !filter.EarliestFictionalDate.IsZero() && !filter.LatestFictionalDate.IsZero() { + query["fictionalDate"] = bson.M{ + "$gte": filter.EarliestFictionalDate, + "$lt": filter.LatestFictionalDate, + } + } else if !filter.LatestFictionalDate.IsZero() { + query["fictionalDate"] = bson.M{ + "$lt": filter.LatestFictionalDate, + } + } else if !filter.EarliestFictionalDate.IsZero() { + query["fictionalDate"] = bson.M{ + "$gte": filter.EarliestFictionalDate, + } + } + + if filter.Category != nil { + query["category"] = *filter.Category + } + + if filter.Open != nil { + query["open"] = *filter.Open + } + + if filter.Unlisted != nil { + query["listed"] = !*filter.Unlisted + } + + limit = filter.Limit + } + + return list(query, limit) +} diff --git a/models/story-category.go b/models/story-category.go new file mode 100644 index 0000000..d72d965 --- /dev/null +++ b/models/story-category.go @@ -0,0 +1,43 @@ +package models + +import ( + "fmt" + "io" +) + +// StoryCategory represents the category of a story. +type StoryCategory string + +const ( + // StoryCategoryInfo is a story category, see GraphQL documentation. + StoryCategoryInfo StoryCategory = "Info" + // StoryCategoryNews is a story category, see GraphQL documentation. + StoryCategoryNews StoryCategory = "News" + // StoryCategoryDocument is a story category, see GraphQL documentation. + StoryCategoryDocument StoryCategory = "Document" + // StoryCategoryBackground is a story category, see GraphQL documentation. + StoryCategoryBackground StoryCategory = "Background" + // StoryCategoryStory is a story category, see GraphQL documentation. + StoryCategoryStory StoryCategory = "Story" +) + +// UnmarshalGQL unmarshals +func (e *StoryCategory) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = StoryCategory(str) + switch *e { + case StoryCategoryInfo, StoryCategoryNews, StoryCategoryDocument, StoryCategoryBackground, StoryCategoryStory: + return nil + default: + return fmt.Errorf("%s is not a valid StoryCategory", str) + } +} + +// MarshalGQL turns it into a JSON string +func (e StoryCategory) MarshalGQL(w io.Writer) { + fmt.Fprint(w, "\""+string(e)+"\"") +} diff --git a/models/story.go b/models/story.go new file mode 100644 index 0000000..8910b39 --- /dev/null +++ b/models/story.go @@ -0,0 +1,18 @@ +package models + +import "time" + +// A Story is user content that does not have a wiki-suitable format. Documents, new stories, short stories, and so on. +// The story model is a container for multiple chapters this time, in contrast to the previous version. +type Story struct { + ID string `bson:"_id"` + Author string `bson:"author"` + Name string `bson:"name"` + Category StoryCategory `bson:"category"` + Open bool `bson:"open"` + Listed bool `bson:"listed"` + Tags []Tag `bson:"tags"` + CreatedDate time.Time `bson:"createdDate"` + FictionalDate time.Time `bson:"fictionalDate,omitempty"` + UpdatedDate time.Time `bson:"updatedDate"` +}