graph2: Added Story type and queries.
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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!
|
||||||
|
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user