Compare commits

...
25 Commits
Author SHA1 Message Date
gisle 576e1df001 UI: Menu and routing works with style reminiscent of aitestory
the build was successful
2018-07-12 22:12:01 +02:00
gisle 7adcdfe1e0 Added change.Timeline, added change submissions to channel mutations, made all change submissions spin off a goroutine
the build was successful
2018-07-08 13:25:18 +02:00
gisle 6f04da49b8 Fixed inconsistencies in code and documentation under resolvers, changed log.Remove to be a method instead of a function as well as take the posts with it.
the build was successful
2018-07-08 12:10:06 +02:00
gisle 9b705e6514 Added change.Submit to addStory, editStory and removeStory
the build was successful
2018-07-08 00:35:15 +02:00
gisle 155309693a Changed removeFile mutation's type to File!
the build was successful
2018-07-08 00:30:07 +02:00
gisle 5895e09f37 Added addStory, addStoryTag, removeStoryTag, editStory and removeStory mutations 2018-07-08 00:28:52 +02:00
gisle 223d9922e9 Changed story.ListTags to ignore tags for unlisted stories 2018-07-07 23:14:17 +02:00
gisle 429387c9ac Added @playlyfe/gql package to UI, added .vscode to gitignore
the build failed
2018-07-07 22:46:54 +02:00
gisle a7113d5efe Added add/edit/remove mutations for Chapter
the build was successful
2018-07-07 17:45:24 +02:00
gisle a0cb8d7fa8 Added tags query 2018-07-07 15:48:14 +02:00
gisle 5189d486a5 Added stories query and resolvers
the build was successful
2018-07-07 15:16:10 +02:00
gisle 011cb00001 Fixed incorrect unmarshalling of results from story.List 2018-07-07 15:15:40 +02:00
gisle 7a52504381 Removed wiki: prefix on authors for imported stories from rpdata-as2storyimport 2018-07-07 15:15:02 +02:00
gisle 866a5056be Added Story.chapters to schema, added Story and Tag resolver
the build was successful
2018-07-07 14:48:15 +02:00
gisle e0409a6635 Fixed all imported stories with rpdata-as2storyimport being unlisted 2018-07-07 13:57:31 +02:00
gisle bae15de3e8 Fixed edit functions not detecting changes correctly 2018-07-07 13:53:11 +02:00
gisle 3f5af44dd6 Fixed error 'Unsupported document type for unmarshalling: []story.Chapter' in story.ListChapterStoryID 2018-07-07 13:48:11 +02:00
gisle 4bbb8bb9ae Fixed typo in commenst for fictionalDate and editedDate resolvers 2018-07-07 13:10:54 +02:00
gisle 87cd5e8e3a Added chapter query and Chapter type resolvers, added functions to get chapters 2018-07-07 12:55:41 +02:00
gisle 136302084d Fixed newline shenanigans in rpdata-as2storyimport
the build was successful
2018-07-07 12:53:52 +02:00
gisle 0122f64fbb Fixed Tags->Tag typo in Story schema
the build was successful
2018-07-07 10:21:10 +02:00
gisle 6bfb9cbe20 Updated packages
the build failed
2018-07-01 21:16:13 +02:00
gisle fca5176579 Changed go base image back to non-alpine version because it lacks git
the build was successful
2018-06-30 20:40:37 +02:00
gisle c4f7846269 Created react app, changed Dockerfile to include UI files
the build failed
2018-06-30 20:38:58 +02:00
gisle 0b6e6b1e9f Fixed ssl not working in container (hopefully)
the build was successful
2018-06-26 11:12:52 +02:00
45 changed files with 14066 additions and 177 deletions
+1
View File
@@ -4,3 +4,4 @@ config.json
api
debug
build
.vscode
+28 -4
View File
@@ -1,5 +1,6 @@
## 1. Server
# Use Go 1.10
FROM golang:1.10
FROM golang:1.10 AS build-server
# Load repository into docker
WORKDIR /go/src/git.aiterp.net/rpdata/api/
@@ -20,7 +21,30 @@ RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-wikifile
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-ensurechannels ./cmd/rpdata-ensurechannels
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-as2storyimport ./cmd/rpdata-as2storyimport
# Move artifacts into final container
FROM alpine:latest
COPY --from=0 /binaries/* /usr/local/bin/
## 2. React UI
# Use Node.JS 18
FROM node:10.5-alpine AS build-id
# The content is already there
WORKDIR /rpdata-ui
COPY ./rpdata-ui /rpdata-ui
# Prepare dependencies
RUN npm install
# Run build
RUN npm run build
## 3. Regroup
# Use alpine linux
FROM alpine:3.7
# Install certificates to enable HTTPS GET requests
RUN apk add --no-cache ca-certificates
# Copy build files into final container
COPY --from=build-server /binaries/* /usr/local/bin/
COPY --from=build-id /rpdata-ui/build /var/www/
# TODO: Use proper server instead of grapihql server
CMD ["/usr/local/bin/rpdata-graphiql"]
+17 -3
View File
@@ -78,23 +78,37 @@ func main() {
}
}
story, err := story.New(result.Name, result.Author, result.Category, false, false, tags, publishDate, fictionalDate)
story, err := story.New(result.Name, result.Author[5:], result.Category, false, false, tags, publishDate, fictionalDate)
if err != nil {
log.Fatalln(err)
}
// Change the story title
title := result.Name
if strings.HasPrefix(result.Source, "#") {
firstNewline := strings.Index(result.Source, "\n")
title = result.Source[1:firstNewline]
result.Name = strings.Replace(result.Source[1:firstNewline], "\r", "", -1)
result.Source = result.Source[firstNewline+1:]
result.Source = strings.Replace(result.Source, "\r\n", "\n", -1)
if strings.HasPrefix(result.Source, "\r") || strings.HasPrefix(result.Source, "\n") {
result.Source = result.Source[1:]
}
}
chapter, err := story.AddChapter(title, result.Author, result.Source, publishDate, fictionalDate)
chapter, err := story.AddChapter(title, result.Author[5:], result.Source, publishDate, fictionalDate)
if err != nil {
log.Fatalln(err)
}
if !result.Unlisted {
listed := true
err := story.Edit(nil, nil, &listed, nil, nil)
if err != nil {
log.Fatalln(err)
}
}
fmt.Println(result.ID, "->", story.ID, chapter.ID)
}
}
+20
View File
@@ -1,6 +1,7 @@
package main
import (
"context"
"encoding/json"
"log"
"net/http"
@@ -8,6 +9,7 @@ import (
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/loader"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/file"
logModel "git.aiterp.net/rpdata/api/model/log"
"git.aiterp.net/rpdata/api/resolver"
@@ -89,6 +91,24 @@ func main() {
w.Write(json)
})
change.InitializeTimeline()
go func() {
index := int64(0)
for {
index++
ch, err := change.Timeline.Get(context.Background(), index)
if err == change.ErrExpired {
continue
} else if err != nil {
log.Println("Timeline:", err)
break
}
log.Printf("Change: time=%s model=%s op=%s id=%s author=%s\n", ch.Time.Local().Format("[Mon Jan 02 15:04 MST]"), ch.Model, ch.Op, ch.ObjectID, ch.Author)
}
}()
log.Fatal(http.ListenAndServe(":17000", nil))
}
+30
View File
@@ -2,6 +2,7 @@ package change
import (
"log"
"sync"
"time"
"git.aiterp.net/rpdata/api/model/counter"
@@ -30,8 +31,13 @@ var PublicModels = []string{
"Post",
}
var submitMutex sync.Mutex
// Submit submits a change to the history.
func Submit(model, op, author, objectID string, data interface{}) (Change, error) {
submitMutex.Lock()
defer submitMutex.Unlock()
index, err := counter.Next("auto_increment", "Change")
if err != nil {
return Change{}, err
@@ -47,6 +53,8 @@ func Submit(model, op, author, objectID string, data interface{}) (Change, error
Data: data,
}
Timeline.push(change)
err = collection.Insert(&change)
if err != nil {
return Change{}, err
@@ -55,6 +63,28 @@ func Submit(model, op, author, objectID string, data interface{}) (Change, error
return change, err
}
// FindID gets a change by ID
func FindID(id int) (Change, error) {
change := Change{}
err := collection.FindId(id).One(&change)
if err != nil {
return Change{}, err
}
return change, nil
}
// List gets all changes in ascending order
func List() ([]Change, error) {
changes := make([]Change, 0, 64)
err := collection.Find(nil).Sort("_id").All(&changes)
if err != nil {
return nil, err
}
return changes, nil
}
func init() {
store.HandleInit(func(db *mgo.Database) {
collection = db.C("common.history")
+118
View File
@@ -0,0 +1,118 @@
package change
import (
"context"
"errors"
"sync"
"github.com/globalsign/mgo"
)
// ErrInvalidIndex is returned when a timeline subscriber tries to get a value
// with a negative id
var ErrInvalidIndex = errors.New("change.timeline: Invalid ID")
// ErrWaitRequired is returned when a timeline subscriber requests an entry that
// has not yet been pushed
var ErrWaitRequired = errors.New("change.timeline: Waiting required")
// ErrExpired is returned when a timeline subscriber requests a change that happened
// too long ago for it to be in the database.
var ErrExpired = errors.New("change.timeline: Change has expired")
type timeline struct {
mutex sync.Mutex
firstIndex int64
currentID int64
cache []Change
notifyCh chan struct{}
}
func (timeline *timeline) Get(ctx context.Context, id int64) (Change, error) {
change, err := timeline.get(id, false)
if err == ErrWaitRequired {
if err := timeline.wait(ctx); err != nil {
return Change{}, err
}
return timeline.get(id, true)
} else if err == mgo.ErrNotFound {
return Change{}, ErrExpired
}
return change, nil
}
func (timeline *timeline) push(change Change) {
timeline.mutex.Lock()
timeline.cache = append(timeline.cache, change)
if len(timeline.cache) > 128 {
copy(timeline.cache, timeline.cache[32:])
timeline.cache = timeline.cache[:len(timeline.cache)-32]
}
// Only update the currentID if this is more recent than the existing ones
if int64(change.ID) > timeline.currentID {
timeline.currentID = int64(change.ID)
}
if timeline.notifyCh != nil {
close(timeline.notifyCh)
timeline.notifyCh = nil
}
timeline.mutex.Unlock()
}
func (timeline *timeline) get(id int64, hasWaited bool) (Change, error) {
if id < 0 {
return Change{}, ErrInvalidIndex
}
timeline.mutex.Lock()
if !hasWaited && id > timeline.currentID {
timeline.mutex.Unlock()
return Change{}, ErrWaitRequired
}
for _, change := range timeline.cache {
if change.ID == int(id) {
timeline.mutex.Unlock()
return change, nil
}
}
timeline.mutex.Unlock()
return FindID(int(id))
}
func (timeline *timeline) wait(ctx context.Context) error {
timeline.mutex.Lock()
if timeline.notifyCh == nil {
timeline.notifyCh = make(chan struct{})
}
ch := timeline.notifyCh
timeline.mutex.Unlock()
select {
case <-ctx.Done():
return ctx.Err()
case <-ch:
return nil
}
}
// Timeline has operations for subscribing to changes in real time
var Timeline = &timeline{}
// InitializeTimeline fills in the timeline with existing entries
func InitializeTimeline() {
changes, err := List()
if err != nil {
return
}
for _, change := range changes {
Timeline.push(change)
}
}
+18 -10
View File
@@ -86,16 +86,6 @@ func List(limit int) ([]Log, error) {
return listLog(bson.M{}, limit)
}
// Remove removes the log post with this ID. Both the long and short ID is accepted
func Remove(id string) error {
return logsCollection.Remove(bson.M{
"$or": []bson.M{
bson.M{"_id": id},
bson.M{"shortId": id},
},
})
}
// ListSearch lists the logs matching the parameters. Empty/zero values means the parameter is ingored when
// building the query. This is the old aitelogs2 way, but with the addition of a text search.
//
@@ -328,6 +318,24 @@ func (log *Log) UpdateCharacters() error {
return nil
}
// Remove removes the log and all associated posts from the database
func (log *Log) Remove() error {
err := logsCollection.Remove(bson.M{"_id": log.ID})
if err != nil {
return err
}
_, err = postCollection.RemoveAll(bson.M{"$or": []bson.M{
bson.M{"logId": log.ID},
bson.M{"logId": log.ShortID},
}})
if err != nil {
return err
}
return nil
}
func findLog(query interface{}) (Log, error) {
log := Log{}
err := logsCollection.Find(query).One(&log)
+20 -1
View File
@@ -41,7 +41,7 @@ func (chapter *Chapter) Edit(title, source *string, fictionalDate *time.Time) er
changes["source"] = *source
changed.Source = *source
}
if fictionalDate != nil && *fictionalDate != chapter.FictionalDate {
if fictionalDate != nil && !fictionalDate.Equal(chapter.FictionalDate) {
changes["fictionalDate"] = *fictionalDate
changed.FictionalDate = *fictionalDate
}
@@ -61,6 +61,25 @@ func (chapter *Chapter) Remove() error {
return chapterCollection.RemoveId(chapter.ID)
}
// FindChapterID finds a chapter by its own ID
func FindChapterID(id string) (Chapter, error) {
chapter := Chapter{}
err := chapterCollection.FindId(id).One(&chapter)
return chapter, err
}
// ListChapterStoryID lists all chapters for the story ID
func ListChapterStoryID(storyID string) ([]Chapter, error) {
chapters := make([]Chapter, 0, 8)
err := chapterCollection.Find(bson.M{"storyId": storyID}).Sort("createdDate").All(&chapters)
if err != nil {
return nil, err
}
return chapters, nil
}
// makeChapterID makes a random chapter ID that's 24 characters long
func makeChapterID() string {
result := "SC"
+11 -6
View File
@@ -84,23 +84,23 @@ func (story *Story) Edit(name, category *string, listed, open *bool, fictionalDa
changes := bson.M{}
changed := *story
if name != nil && *name == story.Name {
if name != nil && *name != story.Name {
changes["name"] = *name
changed.Name = *name
}
if category != nil && *category == story.Category {
if category != nil && *category != story.Category {
changes["category"] = *category
changed.Name = *category
}
if listed != nil && *listed == story.Listed {
if listed != nil && *listed != story.Listed {
changes["listed"] = *listed
changed.Listed = *listed
}
if open != nil && *open == story.Open {
if open != nil && *open != story.Open {
changes["open"] = *open
changed.Open = *open
}
if fictionalDate != nil && *fictionalDate == story.FictionalDate {
if fictionalDate != nil && !fictionalDate.Equal(story.FictionalDate) {
changes["fictionalDate"] = *fictionalDate
changed.FictionalDate = *fictionalDate
}
@@ -124,6 +124,11 @@ func (story *Story) Remove() error {
return storyCollection.RemoveId(story.ID)
}
// Chapters calls ListChapterStoryID with the story's ID:
func (story *Story) Chapters() ([]Chapter, error) {
return ListChapterStoryID(story.ID)
}
// AddChapter adds a chapter to the story. This does not enforce the `Open` setting, but it will log a warning if it
// occurs
func (story *Story) AddChapter(title, author, source string, createdDate, finctionalDate time.Time) (Chapter, error) {
@@ -215,7 +220,7 @@ func List(author string, tags []Tag, earliest, latest time.Time, limit int) ([]S
}
stories := make([]Story, 0, 128)
err := storyCollection.Find(query).Limit(limit).One(&stories)
err := storyCollection.Find(query).Limit(limit).All(&stories)
return stories, err
}
+24
View File
@@ -1,5 +1,12 @@
package story
import (
"sort"
"strings"
"github.com/globalsign/mgo/bson"
)
// A Tag associates a story with other content, like other stories, logs and more.
type Tag struct {
Kind string `bson:"kind"`
@@ -10,3 +17,20 @@ type Tag struct {
func (tag *Tag) Equal(other Tag) bool {
return tag.Kind == other.Kind && tag.Name == other.Name
}
// ListTags lists all tags
func ListTags() ([]Tag, error) {
tags := make([]Tag, 0, 64)
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)
if kindCmp != 0 {
return kindCmp < 0
}
return strings.Compare(tags[i].Name, tags[j].Name) < 0
})
return tags, err
}
+52 -15
View File
@@ -4,6 +4,7 @@ import (
"context"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/channel"
)
@@ -45,58 +46,92 @@ func (r *QueryResolver) Channels(ctx context.Context, args *ChannelsArgs) ([]*Ch
return resolvers, nil
}
// ChannelAddEditInput is input for the addChannel mutation
type ChannelAddEditInput struct {
// ChannelAddArgs is input for the addChannel mutation
type ChannelAddArgs struct {
Input *struct {
Name string
Logged *bool
Hub *bool
EventName *string
LocationName *string
}
}
// AddChannel resolves the addChannel mutation
func (r *MutationResolver) AddChannel(ctx context.Context, args struct{ Input *ChannelAddEditInput }) (*ChannelResolver, error) {
func (r *MutationResolver) AddChannel(ctx context.Context, args *ChannelAddArgs) (*ChannelResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("channel.add") {
return nil, ErrUnauthorized
}
logged := args.Input.Logged != nil && *args.Input.Logged
hub := args.Input.Hub != nil && *args.Input.Hub
logged := input.Logged != nil && *input.Logged
hub := input.Hub != nil && *input.Hub
eventName := ""
if args.Input.EventName != nil {
eventName = *args.Input.EventName
}
locationName := ""
if args.Input.LocationName != nil {
locationName = *args.Input.LocationName
if input.EventName != nil {
eventName = *input.EventName
}
channel, err := channel.New(args.Input.Name, logged, hub, eventName, locationName)
locationName := ""
if input.LocationName != nil {
locationName = *input.LocationName
}
channel, err := channel.New(input.Name, logged, hub, eventName, locationName)
if err != nil {
return nil, err
}
go change.Submit("Channel", "add", user.ID, channel.Name, map[string]interface{}{
"logged": channel.Logged,
"hub": channel.Hub,
"location": input.LocationName,
"event": input.EventName,
})
return &ChannelResolver{C: channel}, nil
}
// ChannelEditArgs is input for the editChannel mutation
type ChannelEditArgs struct {
Input *struct {
Name string
Logged *bool
Hub *bool
EventName *string
LocationName *string
}
}
// EditChannel resolves the editChannel mutation
func (r *MutationResolver) EditChannel(ctx context.Context, args struct{ Input *ChannelAddEditInput }) (*ChannelResolver, error) {
func (r *MutationResolver) EditChannel(ctx context.Context, args *ChannelEditArgs) (*ChannelResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("channel.edit") {
return nil, ErrUnauthorized
}
channel, err := channel.FindName(args.Input.Name)
channel, err := channel.FindName(input.Name)
if err != nil {
return nil, err
}
err = channel.Edit(args.Input.Logged, args.Input.Hub, args.Input.EventName, args.Input.LocationName)
err = channel.Edit(input.Logged, input.Hub, input.EventName, input.LocationName)
if err != nil {
return nil, err
}
go change.Submit("Channel", "edit", user.ID, channel.Name, map[string]interface{}{
"logged": input.Logged,
"hub": input.Hub,
"location": input.LocationName,
"event": input.EventName,
})
return &ChannelResolver{C: channel}, nil
}
@@ -117,6 +152,8 @@ func (r *MutationResolver) RemoveChannel(ctx context.Context, args ChannelArgs)
return nil, err
}
go change.Submit("Channel", "remove", user.ID, channel.Name, nil)
return &ChannelResolver{C: channel}, nil
}
+203
View File
@@ -0,0 +1,203 @@
package resolver
import (
"context"
"time"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/story"
)
// ChapterResolver for the Chapter graphql type
type ChapterResolver struct{ C story.Chapter }
// ChapterArgs is args for chapter query
type ChapterArgs struct {
ID string
}
// Chapter implements the chapter query
func (r *QueryResolver) Chapter(ctx context.Context, args *ChapterArgs) (*ChapterResolver, error) {
chapter, err := story.FindChapterID(args.ID)
if err != nil {
return nil, err
}
return &ChapterResolver{C: chapter}, nil
}
// AddChapterArgs is args for the addChapter mutation
type AddChapterArgs struct {
Input *struct {
StoryID string
Title string
Author *string
Source string
FictionalDate *string
}
}
// AddChapter implements the addChapter mutation
func (r *MutationResolver) AddChapter(ctx context.Context, args *AddChapterArgs) (*ChapterResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "chapter.add") {
return nil, ErrUnauthorized
}
story, err := story.FindID(input.StoryID)
if err != nil {
return nil, err
}
author := user.ID
if input.Author != nil {
author = *input.Author
if user.ID != author && !user.Permitted("chapter.add") {
return nil, ErrPermissionDenied
}
}
fictionalDate := time.Time{}
if input.FictionalDate != nil {
fictionalDate, err = time.Parse(time.RFC3339Nano, *input.FictionalDate)
if err != nil {
return nil, err
}
}
chapter, err := story.AddChapter(input.Title, author, input.Source, time.Now(), fictionalDate)
if err != nil {
return nil, err
}
go change.Submit("Chapter", "add", user.ID, chapter.ID, map[string]interface{}{
"title": input.Title,
"author": author,
"fictionalDate": fictionalDate,
})
return &ChapterResolver{C: chapter}, nil
}
// EditChapterArgs is args for the editChapter mutation
type EditChapterArgs struct {
Input *struct {
ID string
Title *string
Source *string
FictionalDate *string
}
}
// EditChapter implements the editChapter mutation
func (r *MutationResolver) EditChapter(ctx context.Context, args *EditChapterArgs) (*ChapterResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "chapter.edit") {
return nil, ErrUnauthorized
}
chapter, err := story.FindChapterID(input.ID)
if err != nil {
return nil, err
}
if chapter.Author != user.ID && !user.Permitted("chapter.edit") {
return nil, ErrPermissionDenied
}
var fictionalDate *time.Time
if input.FictionalDate != nil {
date, err := time.Parse(time.RFC3339Nano, *input.FictionalDate)
if err != nil {
return nil, err
}
fictionalDate = &date
}
err = chapter.Edit(input.Title, input.Source, fictionalDate)
if err != nil {
return nil, err
}
go change.Submit("Chapter", "edit", user.ID, chapter.ID, map[string]interface{}{
"title": input.Title,
"source": input.Source,
"fictionalDate": fictionalDate,
})
return &ChapterResolver{C: chapter}, nil
}
// DeleteChapterArgs is args for the addChapter mutation
type DeleteChapterArgs struct {
ID string
}
// RemoveChapter implements the removeChapter mutation
func (r *MutationResolver) RemoveChapter(ctx context.Context, args *DeleteChapterArgs) (*ChapterResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "chapter.edit") {
return nil, ErrUnauthorized
}
chapter, err := story.FindChapterID(args.ID)
if err != nil {
return nil, err
}
err = chapter.Remove()
if err != nil {
return nil, err
}
go change.Submit("Chapter", "remove", user.ID, chapter.ID, nil)
return &ChapterResolver{C: chapter}, nil
}
// ID resolves Chapter.id
func (r *ChapterResolver) ID() string {
return r.C.ID
}
// StoryID resolves Chapter.storyId
func (r *ChapterResolver) StoryID() string {
return r.C.StoryID
}
// Title resolves Chapter.title
func (r *ChapterResolver) Title() string {
return r.C.Title
}
// Author resolves Chapter.author
func (r *ChapterResolver) Author() string {
return r.C.Author
}
// Source resolves Chapter.source
func (r *ChapterResolver) Source() string {
return r.C.Source
}
// CreatedDate resolves Chapter.createdDate
func (r *ChapterResolver) CreatedDate() string {
return r.C.CreatedDate.Format(time.RFC3339Nano)
}
// FictionalDate resolves Chapter.fictionalDate
func (r *ChapterResolver) FictionalDate() string {
return r.C.FictionalDate.Format(time.RFC3339Nano)
}
// EditedDate resolves Chapter.editedDate
func (r *ChapterResolver) EditedDate() string {
return r.C.EditedDate.Format(time.RFC3339Nano)
}
+34 -24
View File
@@ -54,7 +54,7 @@ type CharactersArgs struct {
Author *string
}
// Characters resolves
// Characters resolves the characters query
func (r *QueryResolver) Characters(ctx context.Context, args *CharactersArgs) ([]*CharacterResolver, error) {
var chars []character.Character
var err error
@@ -91,7 +91,7 @@ func (r *QueryResolver) Characters(ctx context.Context, args *CharactersArgs) ([
return resolvers, nil
}
// AddCharacterInput is args for mutation addCharacter
// AddCharacterInput is args for the addCharacter mutation
type AddCharacterInput struct {
Nick string
Name string
@@ -102,24 +102,26 @@ type AddCharacterInput struct {
// AddCharacter resolves the addCharacter mutation
func (r *MutationResolver) AddCharacter(ctx context.Context, args struct{ Input *AddCharacterInput }) (*CharacterResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "character.add") {
return nil, ErrUnauthorized
}
nick := args.Input.Nick
name := args.Input.Name
nick := input.Nick
name := input.Name
shortName := ""
if args.Input.ShortName != nil {
shortName = *args.Input.ShortName
if input.ShortName != nil {
shortName = *input.ShortName
} else {
shortName = strings.SplitN(args.Input.Name, " ", 2)[0]
shortName = strings.SplitN(input.Name, " ", 2)[0]
}
author := user.ID
if args.Input.Author != nil {
author = *args.Input.Author
if input.Author != nil {
author = *input.Author
if author != user.ID && !user.Permitted("character.add") {
return nil, ErrPermissionDenied
@@ -127,8 +129,8 @@ func (r *MutationResolver) AddCharacter(ctx context.Context, args struct{ Input
}
description := ""
if args.Input.Description != nil {
description = *args.Input.Description
if input.Description != nil {
description = *input.Description
}
character, err := character.New(nick, name, shortName, author, description)
@@ -155,12 +157,14 @@ type CharacterNickInput struct {
// AddCharacterNick resolves the addCharacterNick mutation
func (r *MutationResolver) AddCharacterNick(ctx context.Context, args struct{ Input *CharacterNickInput }) (*CharacterResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member") {
return nil, ErrUnauthorized
}
character, err := character.FindID(args.Input.ID)
character, err := character.FindID(input.ID)
if err != nil {
return nil, err
}
@@ -168,13 +172,13 @@ func (r *MutationResolver) AddCharacterNick(ctx context.Context, args struct{ In
return nil, ErrPermissionDenied
}
err = character.AddNick(args.Input.Nick)
err = character.AddNick(input.Nick)
if err != nil {
return nil, err
}
go change.Submit("Character", "add.nick", user.ID, character.ID, map[string]interface{}{
"nick": args.Input.Nick,
"nick": input.Nick,
})
log.ScheduleCharacterUpdate()
@@ -184,12 +188,14 @@ func (r *MutationResolver) AddCharacterNick(ctx context.Context, args struct{ In
// RemoveCharacterNick resolves the removeCharacterNick mutation
func (r *MutationResolver) RemoveCharacterNick(ctx context.Context, args struct{ Input *CharacterNickInput }) (*CharacterResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member") {
return nil, ErrUnauthorized
}
character, err := character.FindID(args.Input.ID)
character, err := character.FindID(input.ID)
if err != nil {
return nil, err
}
@@ -197,13 +203,13 @@ func (r *MutationResolver) RemoveCharacterNick(ctx context.Context, args struct{
return nil, ErrPermissionDenied
}
err = character.RemoveNick(args.Input.Nick)
err = character.RemoveNick(input.Nick)
if err != nil {
return nil, err
}
go change.Submit("Character", "remove.nick", user.ID, character.ID, map[string]interface{}{
"nick": args.Input.Nick,
"nick": input.Nick,
})
log.ScheduleCharacterUpdate()
@@ -221,12 +227,14 @@ type CharacterEditInput struct {
// EditCharacter resolves the editCharacter mutation
func (r *MutationResolver) EditCharacter(ctx context.Context, args struct{ Input *CharacterEditInput }) (*CharacterResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member") {
return nil, ErrUnauthorized
}
character, err := character.FindID(args.Input.ID)
character, err := character.FindID(input.ID)
if err != nil {
return nil, err
}
@@ -235,16 +243,18 @@ func (r *MutationResolver) EditCharacter(ctx context.Context, args struct{ Input
}
name := ""
if args.Input.Name != nil {
name = *args.Input.Name
if input.Name != nil {
name = *input.Name
}
shortName := ""
if args.Input.ShortName != nil {
shortName = *args.Input.ShortName
if input.ShortName != nil {
shortName = *input.ShortName
}
description := ""
if args.Input.Description != nil {
description = *args.Input.Description
if input.Description != nil {
description = *input.Description
}
err = character.Edit(name, shortName, description)
+9 -5
View File
@@ -60,21 +60,25 @@ func (r *QueryResolver) Files(ctx context.Context, args *FilesArgs) ([]*FileReso
return resolvers, nil
}
// FileEditInput is an input for the editFile mutation
type FileEditInput struct {
// FileEditArgs is args for the editFile mutation
type FileEditArgs struct {
Input *struct {
ID string
Name *string
Public *bool
}
}
// EditFile resolves the editFile mutation
func (r *MutationResolver) EditFile(ctx context.Context, args *struct{ Input FileEditInput }) (*FileResolver, error) {
func (r *MutationResolver) EditFile(ctx context.Context, args *FileEditArgs) (*FileResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member") {
return nil, ErrUnauthorized
}
file, err := file.FindID(args.Input.ID)
file, err := file.FindID(input.ID)
if err != nil {
return nil, err
}
@@ -82,7 +86,7 @@ func (r *MutationResolver) EditFile(ctx context.Context, args *struct{ Input Fil
return nil, ErrUnauthorized
}
err = file.Edit(args.Input.Name, args.Input.Public)
err = file.Edit(input.Name, input.Public)
if err != nil {
return nil, err
}
+56 -43
View File
@@ -44,18 +44,20 @@ func (r *QueryResolver) Log(ctx context.Context, args *LogArgs) (*LogResolver, e
return &LogResolver{L: l}, nil
}
// LogQueryInput is an input
type LogQueryInput struct {
// LogQueryArgs is args for the logs query
type LogQueryArgs struct {
Input *struct {
Search *string
Characters *[]string
Channels *[]string
Events *[]string
Open *bool
Limit *int32
}
}
// Logs lists logs
func (r *QueryResolver) Logs(ctx context.Context, args *struct{ Input *LogQueryInput }) ([]*LogResolver, error) {
// Logs resolves the logs query
func (r *QueryResolver) Logs(ctx context.Context, args *LogQueryArgs) ([]*LogResolver, error) {
var logs []log.Log
var err error
@@ -65,28 +67,34 @@ func (r *QueryResolver) Logs(ctx context.Context, args *struct{ Input *LogQueryI
// Parse input
limit := 100
search := ""
if input.Search != nil {
search = *input.Search
limit = 0
}
channels := []string(nil)
if input.Channels != nil {
channels = *input.Channels
limit = 0
}
characters := []string(nil)
if input.Characters != nil {
characters = *input.Characters
limit = 0
}
events := []string(nil)
if input.Events != nil {
events = *input.Events
limit = 0
}
if input.Limit != nil {
limit = int(*input.Limit)
}
open := input.Open != nil && *input.Open == true
logs, err = log.ListSearch(search, channels, characters, events, open, limit)
@@ -108,18 +116,22 @@ func (r *QueryResolver) Logs(ctx context.Context, args *struct{ Input *LogQueryI
return resolvers, nil
}
// LogAddInput is an input
type LogAddInput struct {
// LogAddArgs is args for the addLog mutation
type LogAddArgs struct {
Input *struct {
Date string
Channel string
Title *string
Open *bool
Event *string
Description *string
}
}
// AddLog resolves the addLog mutation
func (r *MutationResolver) AddLog(ctx context.Context, args *struct{ Input LogAddInput }) (*LogResolver, error) {
func (r *MutationResolver) AddLog(ctx context.Context, args *LogAddArgs) (*LogResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("log.add") {
return nil, ErrUnauthorized
@@ -131,25 +143,28 @@ func (r *MutationResolver) AddLog(ctx context.Context, args *struct{ Input LogAd
}
title := ""
if args.Input.Title != nil {
title = *args.Input.Title
if input.Title != nil {
title = *input.Title
}
event := ""
if args.Input.Event != nil {
event = *args.Input.Event
}
description := ""
if args.Input.Description != nil {
description = *args.Input.Description
}
open := args.Input.Open != nil && *args.Input.Open == true
log, err := log.New(date, args.Input.Channel, title, event, description, open)
event := ""
if input.Event != nil {
event = *input.Event
}
description := ""
if input.Description != nil {
description = *input.Description
}
open := input.Open != nil && *input.Open == true
log, err := log.New(date, input.Channel, title, event, description, open)
if err != nil {
return nil, err
}
change.Submit("Log", "add", user.ID, log.ID, map[string]interface{}{
go change.Submit("Log", "add", user.ID, log.ID, map[string]interface{}{
"channel": log.Channel,
"title": log.Title,
"event": log.Event,
@@ -160,41 +175,45 @@ func (r *MutationResolver) AddLog(ctx context.Context, args *struct{ Input LogAd
return &LogResolver{L: log}, nil
}
// LogEditInput is an input
type LogEditInput struct {
// LogEditArgs is an input
type LogEditArgs struct {
Input *struct {
ID string
Title *string
Event *string
Description *string
Open *bool
}
}
// EditLog resolves the addLog mutation
func (r *MutationResolver) EditLog(ctx context.Context, args *struct{ Input LogEditInput }) (*LogResolver, error) {
// EditLog resolves the editLog mutation
func (r *MutationResolver) EditLog(ctx context.Context, args *LogEditArgs) (*LogResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("log.edit") {
return nil, ErrUnauthorized
}
l, err := log.FindID(args.Input.ID)
log, err := log.FindID(input.ID)
if err != nil {
return nil, err
}
err = l.Edit(args.Input.Title, args.Input.Event, args.Input.Description, args.Input.Open)
err = log.Edit(input.Title, input.Event, input.Description, input.Open)
if err != nil {
return nil, err
}
change.Submit("Log", "edit", user.ID, l.ID, map[string]interface{}{
"channel": l.Channel,
"title": args.Input.Title,
"event": args.Input.Event,
"description": args.Input.Description,
"open": args.Input.Open,
go change.Submit("Log", "edit", user.ID, log.ID, map[string]interface{}{
"channel": log.Channel,
"title": input.Title,
"event": input.Event,
"description": input.Description,
"open": input.Open,
})
return &LogResolver{L: l}, nil
return &LogResolver{L: log}, nil
}
// RemoveLog resolves the removeLog mutation
@@ -204,25 +223,19 @@ func (r *MutationResolver) RemoveLog(ctx context.Context, args *struct{ ID strin
return nil, ErrUnauthorized
}
l, err := log.FindID(args.ID)
log, err := log.FindID(args.ID)
if err != nil {
return nil, err
}
err = log.Remove(args.ID)
err = log.Remove()
if err != nil {
return nil, err
}
change.Submit("Log", "add", user.ID, l.ID, map[string]interface{}{
"channel": l.Channel,
"title": l.Title,
"event": l.Event,
"description": l.Description,
"open": l.Open,
})
go change.Submit("Log", "remove", user.ID, log.ID, nil)
return &LogResolver{L: l}, nil
return &LogResolver{L: log}, nil
}
// ID resolves Log.id
+37 -25
View File
@@ -47,38 +47,42 @@ func (r *QueryResolver) Posts(ctx context.Context, args *PostsArgs) ([]*PostReso
return resolvers, nil
}
// PostAddInput is an input
type PostAddInput struct {
// PostAddArgs is args for addPost mutation
type PostAddArgs struct {
Input *struct {
LogID string
Time string
Kind string
Nick string
Text string
}
}
// AddPost resolves the addPost mutation
func (r *MutationResolver) AddPost(ctx context.Context, args struct{ Input *PostAddInput }) (*PostResolver, error) {
func (r *MutationResolver) AddPost(ctx context.Context, args *PostAddArgs) (*PostResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("post.add") {
return nil, ErrUnauthorized
}
postTime, err := time.Parse(time.RFC3339Nano, args.Input.Time)
postTime, err := time.Parse(time.RFC3339Nano, input.Time)
if err != nil {
return nil, err
}
log, err := log.FindID(args.Input.LogID)
log, err := log.FindID(input.LogID)
if err != nil {
return nil, err
}
post, err := log.NewPost(postTime, args.Input.Kind, args.Input.Nick, args.Input.Text)
post, err := log.NewPost(postTime, input.Kind, input.Nick, input.Text)
if err != nil {
return nil, err
}
change.Submit("Post", "add", user.ID, post.ID, map[string]interface{}{
go change.Submit("Post", "add", user.ID, post.ID, map[string]interface{}{
"logId": post.LogID,
"time": post.Time,
"kind": post.Kind,
@@ -92,17 +96,21 @@ func (r *MutationResolver) AddPost(ctx context.Context, args struct{ Input *Post
return &PostResolver{P: post}, nil
}
// PostEditInput is an input
type PostEditInput struct {
// PostEditArgs is args for the editPost mutation
type PostEditArgs struct {
Input *struct {
ID string
Time *string
Kind *string
Nick *string
Text *string
}
}
// EditPost resolves the editPost mutation
func (r *MutationResolver) EditPost(ctx context.Context, args struct{ Input *PostEditInput }) (*PostResolver, error) {
func (r *MutationResolver) EditPost(ctx context.Context, args *PostEditArgs) (*PostResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("post.edit") {
return nil, ErrUnauthorized
@@ -110,7 +118,7 @@ func (r *MutationResolver) EditPost(ctx context.Context, args struct{ Input *Pos
postTime := (*time.Time)(nil)
if args.Input.Time != nil {
t, err := time.Parse(time.RFC3339Nano, *args.Input.Time)
t, err := time.Parse(time.RFC3339Nano, *input.Time)
if err != nil {
return nil, err
}
@@ -118,21 +126,21 @@ func (r *MutationResolver) EditPost(ctx context.Context, args struct{ Input *Pos
postTime = &t
}
post, err := log.FindPostID(args.Input.ID)
post, err := log.FindPostID(input.ID)
if err != nil {
return nil, err
}
err = post.Edit(postTime, args.Input.Kind, args.Input.Nick, args.Input.Text)
err = post.Edit(postTime, input.Kind, input.Nick, input.Text)
if err != nil {
return nil, err
}
change.Submit("Post", "edit", user.ID, post.ID, map[string]interface{}{
go change.Submit("Post", "edit", user.ID, post.ID, map[string]interface{}{
"time": postTime,
"kind": args.Input.Kind,
"nick": args.Input.Nick,
"text": args.Input.Text,
"kind": input.Kind,
"nick": input.Nick,
"text": input.Text,
})
go log.UpdateCharacters(post.LogID)
@@ -140,32 +148,36 @@ func (r *MutationResolver) EditPost(ctx context.Context, args struct{ Input *Pos
return &PostResolver{P: post}, nil
}
// PostMoveInput is an input
type PostMoveInput struct {
// PostMoveArgs is args for movePost mutation
type PostMoveArgs struct {
Input *struct {
ID string
ToPosition int32
}
}
// MovePost resolves the movePost mutation
func (r *MutationResolver) MovePost(ctx context.Context, args struct{ Input *PostMoveInput }) (*PostResolver, error) {
func (r *MutationResolver) MovePost(ctx context.Context, args *PostMoveArgs) (*PostResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("post.move") {
return nil, ErrUnauthorized
}
post, err := log.FindPostID(args.Input.ID)
post, err := log.FindPostID(input.ID)
if err != nil {
return nil, err
}
err = post.Move(int(args.Input.ToPosition))
err = post.Move(int(input.ToPosition))
if err != nil {
return nil, err
}
change.Submit("Post", "move", user.ID, post.ID, map[string]interface{}{
go change.Submit("Post", "move", user.ID, post.ID, map[string]interface{}{
"logId": post.LogID,
"targetIndex": args.Input.ToPosition,
"targetIndex": input.ToPosition,
})
return &PostResolver{P: post}, nil
@@ -188,7 +200,7 @@ func (r *MutationResolver) RemovePost(ctx context.Context, args PostRemoveArgs)
return nil, err
}
change.Submit("Post", "remove", user.ID, post.ID, map[string]interface{}{
go change.Submit("Post", "remove", user.ID, post.ID, map[string]interface{}{
"logId": post.LogID,
})
+400
View File
@@ -1 +1,401 @@
package resolver
import (
"context"
"time"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/story"
)
// StoryResolver for the Story graphql type
type StoryResolver struct{ S story.Story }
// StoryArgs is args for story query
type StoryArgs struct {
ID string
}
// Story implements the story query
func (r *QueryResolver) Story(ctx context.Context, args *StoryArgs) (*StoryResolver, error) {
story, err := story.FindID(args.ID)
if err != nil {
return nil, err
}
return &StoryResolver{S: story}, nil
}
// StoriesArg is args for stories query
type StoriesArg struct {
Input *struct {
Author *string
Tags *[]TagInput
EarliestFictionalDate *string
LatestFictionalDate *string
Limit *int32
}
}
// Stories implements the stories query
func (r *QueryResolver) Stories(ctx context.Context, args *StoriesArg) ([]*StoryResolver, error) {
input := args.Input
author := ""
if input.Author != nil {
author = *input.Author
}
tags := make([]story.Tag, 0, 8)
if input.Tags != nil {
for _, tagInput := range *input.Tags {
tags = append(tags, story.Tag{
Kind: tagInput.Kind,
Name: tagInput.Name,
})
}
}
earliest := time.Time{}
err := error(nil)
if input.EarliestFictionalDate != nil {
earliest, err = time.Parse(time.RFC3339Nano, *input.EarliestFictionalDate)
if err != nil {
return nil, err
}
}
latest := time.Time{}
if input.LatestFictionalDate != nil {
latest, err = time.Parse(time.RFC3339Nano, *input.LatestFictionalDate)
if err != nil {
return nil, err
}
}
limit := 30
if input.Limit != nil {
limit = int(*input.Limit)
}
stories, err := story.List(author, tags, earliest, latest, limit)
if err != nil {
return nil, err
}
resolvers := make([]*StoryResolver, len(stories))
for i, story := range stories {
resolvers[i] = &StoryResolver{S: story}
}
return resolvers, nil
}
// StoryAddArgs is args for the addStory mutation
type StoryAddArgs struct {
Input *struct {
Name string
Category string
Author *string
Open *bool
Listed *bool
Tags *[]TagInput
FictionalDate *string
}
}
// AddStory implements the addStory mutation
func (r *MutationResolver) AddStory(ctx context.Context, args *StoryAddArgs) (*StoryResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "story.add") {
return nil, ErrUnauthorized
}
author := user.ID
if input.Author != nil {
author = *input.Author
if user.ID != author && !user.Permitted("story.add") {
return nil, ErrPermissionDenied
}
}
listed := (input.Listed != nil && *input.Listed == true)
open := (input.Open != nil && *input.Open == true)
tags := make([]story.Tag, 0, 8)
if input.Tags != nil {
for _, tagInput := range *input.Tags {
tags = append(tags, story.Tag{
Kind: tagInput.Kind,
Name: tagInput.Name,
})
}
}
fictionalDate := time.Time{}
if input.FictionalDate != nil {
date, err := time.Parse(time.RFC3339Nano, *input.FictionalDate)
if err != nil {
return nil, err
}
fictionalDate = date
}
story, err := story.New(input.Name, author, input.Category, listed, open, tags, time.Now(), fictionalDate)
if err != nil {
return nil, err
}
go change.Submit("Story", "add", user.ID, story.ID, map[string]interface{}{
"name": input.Name,
"category": input.Category,
"author": input.Author,
})
return &StoryResolver{S: story}, nil
}
// StoryTagAddArgs is args for the addStoryTag mutation
type StoryTagAddArgs struct {
Input *struct {
ID string
Tag TagInput
}
}
// AddStoryTag implements the addStoryTag mutation
func (r *MutationResolver) AddStoryTag(ctx context.Context, args *StoryTagAddArgs) (*StoryResolver, error) {
input := args.Input
tag := story.Tag{Kind: input.Tag.Kind, Name: input.Tag.Name}
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "story.edit") {
return nil, ErrUnauthorized
}
story, err := story.FindID(input.ID)
if err != nil {
return nil, err
}
if story.Author != user.ID && !user.Permitted("story.edit") {
return nil, ErrPermissionDenied
}
err = story.AddTag(tag)
if err != nil {
return nil, err
}
go change.Submit("Story", "add.tag", user.ID, story.ID, map[string]interface{}{
"kind": tag.Kind,
"name": tag.Name,
})
return &StoryResolver{S: story}, nil
}
// StoryTagRemoveArgs is args for the removeStoryTag mutation
type StoryTagRemoveArgs struct {
Input *struct {
ID string
Tag TagInput
}
}
// RemoveStoryTag implements the removeStoryTag mutation
func (r *MutationResolver) RemoveStoryTag(ctx context.Context, args *StoryTagRemoveArgs) (*StoryResolver, error) {
input := args.Input
tag := story.Tag{Kind: input.Tag.Kind, Name: input.Tag.Name}
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "story.edit") {
return nil, ErrUnauthorized
}
story, err := story.FindID(input.ID)
if err != nil {
return nil, err
}
if story.Author != user.ID && !user.Permitted("story.edit") {
return nil, ErrPermissionDenied
}
err = story.RemoveTag(tag)
if err != nil {
return nil, err
}
go change.Submit("Story", "remove.tag", user.ID, story.ID, map[string]interface{}{
"kind": tag.Kind,
"name": tag.Name,
})
return &StoryResolver{S: story}, nil
}
// StoryEditArgs is args for the addStory mutation
type StoryEditArgs struct {
Input *struct {
ID string
Name *string
Category *string
Author *string
Open *bool
Listed *bool
FictionalDate *string
}
}
// EditStory implements the editStory mutation
func (r *MutationResolver) EditStory(ctx context.Context, args *StoryEditArgs) (*StoryResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "story.edit") {
return nil, ErrUnauthorized
}
story, err := story.FindID(input.ID)
if err != nil {
return nil, err
}
if story.Author != user.ID && !user.Permitted("story.edit") {
return nil, ErrPermissionDenied
}
var fictionalDate *time.Time
if input.FictionalDate != nil {
date, err := time.Parse(time.RFC3339Nano, *input.FictionalDate)
if err != nil {
return nil, err
}
fictionalDate = &date
}
err = story.Edit(input.Name, input.Category, input.Listed, input.Open, fictionalDate)
if err != nil {
return nil, err
}
go change.Submit("Story", "edit", user.ID, story.ID, map[string]interface{}{
"name": input.Name,
"category": input.Category,
"author": input.Author,
"open": input.Open,
"listed": input.Listed,
"fictionalDate": input.FictionalDate,
})
return &StoryResolver{S: story}, nil
}
// StoryRemoveArgs is args for the removeStory mutation
type StoryRemoveArgs struct {
ID string
}
// RemoveStory implements the removeStory mutation
func (r *MutationResolver) RemoveStory(ctx context.Context, args *StoryRemoveArgs) (*StoryResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "story.edit") {
return nil, ErrUnauthorized
}
story, err := story.FindID(args.ID)
if err != nil {
return nil, err
}
if story.Author != user.ID && !user.Permitted("story.remove") {
return nil, ErrPermissionDenied
}
err = story.Remove()
if err != nil {
return nil, err
}
go change.Submit("Story", "remove", user.ID, story.ID, nil)
return &StoryResolver{S: story}, nil
}
// ID resolves Story.id
func (r *StoryResolver) ID() string {
return r.S.ID
}
// Author resolves Story.author
func (r *StoryResolver) Author() string {
return r.S.Author
}
// Name resolves Story.name
func (r *StoryResolver) Name() string {
return r.S.Name
}
// Category resolves Story.category
func (r *StoryResolver) Category() string {
return r.S.Category
}
// Open resolves Story.open
func (r *StoryResolver) Open() bool {
return r.S.Open
}
// Listed resolves Story.listed
func (r *StoryResolver) Listed() bool {
return r.S.Listed
}
// Tags resolves Story.tags
func (r *StoryResolver) Tags() []*TagResolver {
resolvers := make([]*TagResolver, len(r.S.Tags))
for i, tag := range r.S.Tags {
resolvers[i] = &TagResolver{T: tag}
}
return resolvers
}
// Chapters resolves Story.chapters
func (r *StoryResolver) Chapters() ([]*ChapterResolver, error) {
chapters, err := r.S.Chapters()
if err != nil {
return nil, err
}
resolvers := make([]*ChapterResolver, len(chapters))
for i, chapter := range chapters {
resolvers[i] = &ChapterResolver{C: chapter}
}
return resolvers, nil
}
// CreatedDate resolves Story.createdDate
func (r *StoryResolver) CreatedDate() string {
return r.S.CreatedDate.Format(time.RFC3339Nano)
}
// FictionalDate resolves Story.fictionalDate
func (r *StoryResolver) FictionalDate() string {
return r.S.FictionalDate.Format(time.RFC3339Nano)
}
// UpdatedDate resolves Story.updatedDate
func (r *StoryResolver) UpdatedDate() string {
return r.S.UpdatedDate.Format(time.RFC3339Nano)
}
+37
View File
@@ -0,0 +1,37 @@
package resolver
import "git.aiterp.net/rpdata/api/model/story"
// TagResolver for the Tag graphql type
type TagResolver struct{ T story.Tag }
// TagInput resolves the TagInput input
type TagInput struct {
Kind string
Name string
}
// Tags implements the tags query
func (r *QueryResolver) Tags() ([]*TagResolver, error) {
tags, err := story.ListTags()
if err != nil {
return nil, err
}
resolvers := make([]*TagResolver, len(tags))
for i, tag := range tags {
resolvers[i] = &TagResolver{T: tag}
}
return resolvers, nil
}
// Kind resolves Tag.kind
func (r *TagResolver) Kind() string {
return r.T.Kind
}
// Name resolves Tag.name
func (r *TagResolver) Name() string {
return r.T.Name
}
+21
View File
@@ -0,0 +1,21 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+14
View File
@@ -0,0 +1,14 @@
# User Interface
## Setup
This is a UI created with `create-react-app`, so the process of getting started on a new host is navigating to the directory and running these commands.
```sh
npm install
npm start
```
## Release
This is built and packaged with the parent project.
+12036
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
{
"name": "rpdata-ui",
"version": "0.1.0",
"private": true,
"dependencies": {
"ajv": "^6.5.2",
"apollo-boost": "^0.1.10",
"graphql": "^0.13.2",
"graphql-tag": "^2.9.2",
"pluralize": "^7.0.0",
"react": "^16.4.1",
"react-apollo": "^2.1.9",
"react-dom": "^16.4.1",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"react-scripts": "1.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"devDependencies": {
"@playlyfe/gql": "^2.6.0"
},
"proxy": {
"/graphql": {
"target": "http://127.0.0.1:17000"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

+40
View File
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Aite RP</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
+15
View File
@@ -0,0 +1,15 @@
{
"short_name": "Aite RP",
"name": "Aite RP Website (aiterp.net)",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#000000"
}
+23
View File
@@ -0,0 +1,23 @@
import React, { Component } from 'react'
import { Route, Router, Redirect, Switch } from "react-router-dom";
import StoryRoot from './routes/story/StoryRoot';
import LogsIndex from './routes/logs/LogsIndex';
class App extends Component {
render() {
return (
<div className="App">
<Switch>
<Redirect exact path="/" to="/story/" />
<Route path="/story/" component={StoryRoot}/>
<Route path="/logs/" component={LogsIndex}/>
</Switch>
</div>
);
}
}
export default App;
/* See ./common/Menu.js to change the top navbar */
+13
View File
@@ -0,0 +1,13 @@
import { ApolloClient } from "apollo-client"
import { HttpLink } from "apollo-link-http"
import { InMemoryCache } from "apollo-cache-inmemory"
const client = new ApolloClient({
link: new HttpLink({
uri: "/graphql"
}),
cache: new InMemoryCache(),
})
export default client
+76
View File
@@ -0,0 +1,76 @@
/* /story/ */
.theme-story .color-menu {
color: #789;
}
.theme-story .MenuLink.selected {
background-color: rgba(34, 187, 255, 0.125);
}
.theme-story .MenuLink:hover, .theme-story .Menu > .head-menu > a:hover {
background-color: rgba(34, 187, 255, 0.25);
}
.theme-story .color-primary {
color: #1CF;
}
.theme-story .color-text {
color: #ABC;
}
/* /logs/ */
.theme-logs .color-menu {
color: #998;
}
.theme-logs .MenuLink.selected {
background-color: rgba(255, 187, 34, 0.125);
}
.theme-logs .MenuLink:hover, .theme-logs .Menu > .head-menu > a:hover {
background-color: rgba(255, 187, 34, 0.25);
}
.theme-logs .color-primary {
color: #FC1;
}
.theme-logs .color-text {
color: #CCB;
}
/* /data/ */
.theme-data .color-menu {
color: #898;
}
.theme-data .MenuLink.selected {
background-color: rgba(35, 255, 128, 0.125);
}
.theme-data .MenuLink:hover, .theme-data .Menu > .head-menu > a:hover {
background-color: rgba(35, 255, 128, 0.25);
}
.theme-data .color-primary {
color: #1F8;
}
.theme-data .color-text {
color: #ACB;
}
/* Tag colors */
.color-tag-event {
color: #B77;
}
.color-tag-location {
color: #7B7;
}
.color-tag-organization {
color: #B7B;
}
.color-tag-series {
color: #BB7;
}
.color-tag-character {
color: #37B;
}
/* Danger */
.color-danger {
color: #FA3;
}
.MenuLink:hover, .MenuLink:hover .text, .Menu > .head-menu > a:hover {
color: #EEE !important;
}
+42
View File
@@ -0,0 +1,42 @@
import React, { Component } from 'react';
import './css/Background.css';
/**
* Background renders a full-size image that detects whether the source is
* landscape or portrait. It's a react-ification of AiteStory's background.
*/
export default class Background extends Component {
/**
* @param {HTMLImageElement} img
*/
onRef(img) {
if (img == null) {
return
}
if (img.complete && typeof(img.naturalHeight) !== 'undefined' && img.naturalHeight !== 0) {
const width = img.clientWidth
const height = img.clientHeight
if(width > (height * 1.50)) {
img.className += ' landscape'
}
} else {
img.onload = function () {
const width = img.clientWidth
const height = img.clientHeight
if(width > (height * 1.50)) {
img.className += ' landscape'
}
}
}
}
render() {
const src = this.props.src || "/assets/images/bg.png"
const opacity = this.props.opacity || 0.25
return <img style={{filter: `brightness(${(Number(opacity)) * 100}%)`}} ref={this.onRef.bind(this)} className="Background" src={src} />
}
}
+60
View File
@@ -0,0 +1,60 @@
import React, { Component } from 'react';
import { Link } from "react-router-dom";
import './css/Menu.css';
export default class Menu extends Component {
render() {
const { children } = this.props
return (
<nav className="Menu">
<h1 className="color-primary">Aite RP</h1>
<div className="head-menu">
<HeadMenuLink to="/story/">Story</HeadMenuLink>
<HeadMenuLink to="/logs/">Logs</HeadMenuLink>
</div>
<div className="content">
{ children }
</div>
</nav>
)
}
}
export function MenuLink({ icon, children, tagType, to, onClick }) {
const isSelected = window.location.pathname === to
const className = isSelected ? "selected" : "not-selected"
const textClass = isSelected ? "color-primary" : `color-tag-${tagType||"none"}`
const body = (
<div className={`MenuLink color-menu ${className}`} onClick={onClick}>
<div className="icon color-primary">{icon}</div>
<div className={`text ${textClass}`}>{children}</div>
</div>
)
if (to == null) {
return body
} else {
return <Link to={to}>{body}</Link>
}
}
export function MenuGap() {
return <div className="MenuGap"></div>
}
export function MenuHeader({children}) {
return <div className="MenuHeader color-menu">{children}</div>
}
export function HeadMenuLink({to, children}) {
const className = (window.location.pathname.startsWith(to) ? "color-primary" : "color-menu")
return <Link className={className} to={to}>{children}</Link>
}
export function MenuBlurb({children}) {
return <div className="MenuBlurb">{children}</div>
}
+22
View File
@@ -0,0 +1,22 @@
img.Background {
/* Set rules to fill background */
width: 100%;
/* Set up proportionate scaling */
min-height: 100%;
height: auto;
/* Set up positioning */
position: fixed;
top: 0;
left: 0;
z-index: -1;
user-select: none;
-webkit-user-drag: none;
}
img.Background.landscape {
/* Set rules to fill background */
height: 100%;
/* Set up proportionate scaling */
min-width: 100%;
width: auto;
}
+64
View File
@@ -0,0 +1,64 @@
.Menu {
position: sticky;
width: 28ch;
text-align: center;
}
.Menu > .head-menu {
padding: 0 2ch;
padding-bottom: 0.5em;
}
.Menu > .head-menu > a {
display: inline-block;
padding: 0.25em 1ch;
font-size: 0.75em;
}
.Menu > h1 {
padding: 1em 0 0em 0;
margin: 0;
font-size: 2.5em;
user-select: none;
}
.Menu > .content {
margin-top: 1em;
text-align: left;
}
.MenuLink {
width: 100%;
padding-left: 0.5ch;
font-size: 1.1em;
cursor: pointer;
}
.MenuLink .icon {
display: inline-block;
width: 3ch;
padding: 0.15em 0.125em;
text-align: center;
}
.MenuLink .text {
display: inline-block;
}
.MenuGap {
padding: 0.25em;
}
.MenuHeader {
opacity: 0.5;
user-select: none;
padding: 0.125em 1.5ch;
}
.MenuBlurb {
opacity: 0.5;
padding: 0.25em 1.5ch;
}
+11
View File
@@ -0,0 +1,11 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
background-color: black;
color: white;
}
a {
text-decoration: none;
}
+21
View File
@@ -0,0 +1,21 @@
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom"
import { ApolloProvider } from "react-apollo";
import client from "./client";
import App from "./App"
import registerServiceWorker from "./registerServiceWorker"
import "./index.css"
import "./colors.css";
ReactDOM.render((
<ApolloProvider client={client}>
<BrowserRouter>
<App />
</BrowserRouter>
</ApolloProvider>
), document.getElementById('root'))
registerServiceWorker();
+117
View File
@@ -0,0 +1,117 @@
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://goo.gl/SC7cgQ'
);
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
+14
View File
@@ -0,0 +1,14 @@
import React, { Component } from 'react';
import Background from '../../common/Background';
import LogsMenu from './LogsMenu';
export default class LogsIndex extends Component {
render() {
return (
<div className="StoryIndex theme-logs">
<Background src="https://wiki.aiterp.net/images/c/ce/Ehanis_Tioran_-_Portrait_-_Resized.png" />
<LogsMenu />
</div>
)
}
}
+17
View File
@@ -0,0 +1,17 @@
import React, { Component } from 'react';
import Menu, { MenuHeader, MenuLink, MenuGap } from '../../common/Menu';
export default class LogsMenu extends Component {
render() {
return (
<Menu>
<MenuHeader>Logs</MenuHeader>
<MenuLink to="/logs/" icon="R">Recent</MenuLink>
<MenuGap />
<MenuHeader>Options</MenuHeader>
<MenuLink icon="+">Create Log</MenuLink>
<MenuLink icon="I">Import Log</MenuLink>
</Menu>
)
}
}
+42
View File
@@ -0,0 +1,42 @@
import React, { Component } from "react";
import Menu, { MenuHeader, MenuLink, MenuGap } from "../../common/Menu";
import pluralize from "pluralize"
export default class StoryMenu extends Component {
render() {
return (
<Menu>
<MenuHeader>Story</MenuHeader>
<MenuLink to="/story/" icon="A">All</MenuLink>
<MenuGap />
<MenuHeader>By Category</MenuHeader>
<CategoryLinks categories={this.props.categories} />
<MenuGap />
<MenuHeader>By Tag</MenuHeader>
<MenuLink to="/story/tag/Location/Miner's%20Respite" icon="L" tagType="location">Miner's Respite</MenuLink>
<MenuLink to="/story/tag/Chracter/Ilyna%20T'Rea" icon="C" tagType="character">Ilyna T'Rea</MenuLink>
<MenuLink to="/story/tag/Organization/Redrock%20Agency" icon="O" tagType="organization">Redrock Agency</MenuLink>
<MenuLink to="/story/tag/Series/Yellow%20Haze" icon="S" tagType="series">Yellow Haze</MenuLink>
<MenuLink to="/story/tag/Event/Surveying%20The%20Ice%20World" icon="E" tagType="event">Surveying the Ice World</MenuLink>
<MenuLink to="/story/tag/" icon="T">All Tags</MenuLink>
<MenuGap />
<MenuHeader>Your Content</MenuHeader>
<MenuLink to="/story/authored/" icon="A">Listed</MenuLink>
<MenuLink to="/story/open/" icon="O">Open</MenuLink>
<MenuLink to="/story/unlisted/" icon="U">Unlisted</MenuLink>
<MenuGap />
<MenuHeader>Options</MenuHeader>
<MenuLink icon="+">Create Story</MenuLink>
</Menu>
)
}
}
function CategoryLinks({categories}) {
return categories.map(category => {
const icon = category.charAt(0).toUpperCase()
const label = pluralize(category)
return <MenuLink to={`/story/category/${category}`} icon={icon}>{label}</MenuLink>
})
}
+31
View File
@@ -0,0 +1,31 @@
import React, { Component } from 'react';
import { Query } from 'react-apollo';
import Background from '../../common/Background';
import StoryMenu from './StoryMenu';
import INDEX_QUERY from "./gql/StoryIndex";
export class StoryIndex extends Component {
render() {
const categories = this.props.categoryType.enumValues.map(ev => ev.name).sort()
return (
<div className="StoryIndex theme-story">
<Background />
<StoryMenu categories={categories} />
</div>
)
}
}
export default () => (
<Query query={INDEX_QUERY}>
{({ loading, error, data }) => {
if (loading) return 'Loading...';
if (error) return `Error! ${error.message}`;
return <StoryIndex {...data} />
}}
</Query>
);
@@ -0,0 +1,23 @@
import gql from "graphql-tag"
export default gql`
query StoryIndex {
categoryType: __type(name: "StoryCategory") {
enumValues {
name
description
}
}
stories(input: {}) {
id
name
author
open
tags {
kind
name
}
}
}
`
+42 -1
View File
@@ -36,6 +36,21 @@ type Query {
files(includePublic: Boolean, mimeTypes: [String!]): [File!]!
# Find story by ID
story(id: String!): Story
# Find stories
stories(input: StoriesQueryInput!): [Story!]!
# Find story chapter by ID
chapter(id: String!): Chapter
# Find all distinct tags used in stories
tags: [Tag!]!
# Find current session
session: Session!
}
@@ -95,7 +110,33 @@ type Mutation {
editFile(input: EditFileInput!): File!
# Remove a file
removeFile(id: String!): File
removeFile(id: String!): File!
# Add a story
addStory(input: StoryAddInput!): Story!
# Add a story tag
addStoryTag(input: StoryTagAddInput!): Story!
# Remove a story tag
removeStoryTag(input: StoryTagRemoveInput!): Story!
# Edit a story
editStory(input: StoryEditInput!): Story!
# Remove a story
removeStory(id: String!): Story!
# Add a chapter to a story
addChapter(input: ChapterAddInput!): Chapter!
# Edit a chapter
editChapter(input: ChapterEditInput!): Chapter!
# Remove a chapter
removeChapter(id: String!): Chapter!
# Log in
+36
View File
@@ -21,3 +21,39 @@ type Chapter {
# The date of edit.
editedDate: String!
}
# Input for addChapter mutation
input ChapterAddInput {
# The story to add a chapter to
storyId: String!
# The title to give the chapter. It can not be left empty
title: String!
# The author to set for the chapter. It will use the logged in user ID, and only
# users with appropriate permissions can post as another author. Even then, it's only
# for importing/moving content. This author name will not be used when logging the
# submission
author: String
# The markdown source for the content
source: String!
# Optionally, assign a fictional date for the chapter
fictionalDate: String
}
# Input for editChapter mutation
input ChapterEditInput {
# The chapter to edit
id: String!
# Change the chapter's title
title: String
# Change the source
source: String
# Set the fictional date for a chapter
fictionalDate: String
}
+92 -2
View File
@@ -20,7 +20,10 @@ type Story {
category: StoryCategory!
# The tags for this tory
tags: [Tags!]!
tags: [Tag!]!
# The chapters of this story
chapters: [Chapter!]!
# The date the story was created (RFC3339 format).
createdDate: String!
@@ -29,7 +32,94 @@ type Story {
fictionalDate: String!
# The date of the last major update to the story (RFC3339 format). This being bumped means a new chapter has been added.
fictionalDate: String!
updatedDate: String!
}
input StoriesQueryInput {
# What author to query for
author: String
# What tags to query for
tags: [TagInput!]
# The earliest fictionalDate
earliestFictionalDate: String
# The latest fictionalDate
latestFictionalDate: String
# 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 (RFC3339 format).
fictionalDate: String
}
# 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 (RFC3339 format).
fictionalDate: String
}
# 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
+9
View File
@@ -7,6 +7,15 @@ type Tag {
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...