Browse Source

graph2: Added Story type and queries.

1.0
Gisle Aune 6 years ago
parent
commit
79171bf9a2
  1. 11
      graph2/gqlgen.yml
  2. 4
      graph2/graph.go
  3. 31
      graph2/queries/story.go
  4. 7
      graph2/schema/root.gql
  5. 151
      graph2/schema/types/Story.gql
  6. 27
      graph2/types/story.go
  7. 59
      models/stories/db.go
  8. 11
      models/stories/find.go
  9. 67
      models/stories/list.go
  10. 43
      models/story-category.go
  11. 18
      models/story.go

11
graph2/gqlgen.yml

@ -11,6 +11,8 @@ model:
models: models:
Tag: Tag:
model: git.aiterp.net/rpdata/api/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: TagKind:
model: git.aiterp.net/rpdata/api/models.TagKind model: git.aiterp.net/rpdata/api/models.TagKind
Character: Character:
@ -34,6 +36,15 @@ models:
fields: fields:
fictionalDate: fictionalDate:
resolver: true 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: File:
model: git.aiterp.net/rpdata/api/models.File model: git.aiterp.net/rpdata/api/models.File
FilesFilter: FilesFilter:

4
graph2/graph.go

@ -30,6 +30,10 @@ func (r *rootResolver) Chapter() ChapterResolver {
return &types.ChapterResolver return &types.ChapterResolver
} }
func (r *rootResolver) Story() StoryResolver {
return &types.StoryResolver
}
func (r *rootResolver) File() FileResolver { func (r *rootResolver) File() FileResolver {
return &types.FileResolver return &types.FileResolver
} }

31
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)
}

7
graph2/schema/root.gql

@ -39,6 +39,13 @@ type Query {
tags: [Tag!]! tags: [Tag!]!
# Find story by ID
story(id: String!): Story!
# Find stories
stories(filter: StoriesFilter): [Story!]!
# Find file by ID # Find file by ID
file(id: String!): File! file(id: String!): File!

151
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
}

27
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

59
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")
})
}

11
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})
}

67
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)
}

43
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)+"\"")
}

18
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"`
}
Loading…
Cancel
Save