Gisle Aune
6 years ago
11 changed files with 429 additions and 0 deletions
-
11graph2/gqlgen.yml
-
4graph2/graph.go
-
31graph2/queries/story.go
-
7graph2/schema/root.gql
-
151graph2/schema/types/Story.gql
-
27graph2/types/story.go
-
59models/stories/db.go
-
11models/stories/find.go
-
67models/stories/list.go
-
43models/story-category.go
-
18models/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) |
|||
} |
@ -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 |
|||
} |
@ -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 |
@ -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") |
|||
}) |
|||
} |
@ -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}) |
|||
} |
@ -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) |
|||
} |
@ -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)+"\"") |
|||
} |
@ -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"` |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue