Compare commits

...
17 Commits
Author SHA1 Message Date
gisle aff0d99842 graph2: Added exception for open logs query in complexity.
the build was successful
2019-04-27 11:37:01 +02:00
gisle 00150118d2 graph2: Added complexity to prevent queries that can take down the server.
the build was successful
2019-04-25 20:47:20 +02:00
gisle 332a1eb32b Fixed rpdata-as2storyimport not compiling. 2019-03-17 21:32:21 +01:00
gisle db4d46c368 Added story to comment changes.
the build was successful
2019-03-17 20:44:30 +01:00
gisle a8c8610ca2 models: Fixed wrong database mapping for comment source.
the build was successful
2019-01-17 21:19:13 +01:00
gisle d09c17d30d graph2: Changed addComment error message on can-comment check to be consistent with others.
the build was successful
2019-01-16 22:07:00 +01:00
gisle e3e3ea07db graph2: Changed Chapter to not return any comments if mode is disabled, even if there are comments.
the build was successful
2019-01-16 22:05:40 +01:00
gisle 22592439da graph2, models: Added removeComment mutation.
the build was successful
2019-01-16 22:03:28 +01:00
gisle dfa9fb4025 graph2: Changed Comment to return null instead of a zero-date if it's cleared.
the build was successful
2019-01-16 21:50:53 +01:00
gisle 0e2a68a2d4 graph2, models: Added editComment mutation. 2019-01-16 21:45:00 +01:00
gisle 1eefa65527 models: Fixed wrong database mapping for comment id. 2019-01-16 21:41:36 +01:00
gisle f9b6af58c9 graph2: Changed Comment character resolver to use loader.
the build was successful
2019-01-15 21:42:44 +01:00
gisle 56ecf48b57 graph2, models: Added addComment mutation.
the build was successful
2019-01-15 21:34:57 +01:00
gisle 06fbfc90c5 graph2, model: Added Comment type to Change. 2019-01-14 19:58:05 +01:00
gisle 39a4fef506 models, graph2: Added comment removal on chapter removal.
the build was successful
2019-01-14 18:01:43 +01:00
gisle f9fe0bb5f8 models, graph2: Added comment options to chapter mutations.
the build was successful
2019-01-14 17:55:38 +01:00
gisle d22ab387dd models, graph2: Added Comment type and queries, and hooked it up to chapter.
the build was successful
2019-01-14 17:03:16 +01:00
29 changed files with 905 additions and 29 deletions
+1 -1
View File
@@ -104,7 +104,7 @@ func main() {
} }
} }
chapter, err := chapters.Add(story, title, result.Author[5:], result.Source, publishDate, &fictionalDate) chapter, err := chapters.Add(story, title, result.Author[5:], result.Source, publishDate, &fictionalDate, models.ChapterCommentModeDisabled)
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
+6 -5
View File
@@ -8,15 +8,13 @@ import (
"runtime/debug" "runtime/debug"
"strings" "strings"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/changes"
"git.aiterp.net/rpdata/api/models/logs"
"git.aiterp.net/rpdata/api/graph2" "git.aiterp.net/rpdata/api/graph2"
"git.aiterp.net/rpdata/api/internal/auth" "git.aiterp.net/rpdata/api/internal/auth"
"git.aiterp.net/rpdata/api/internal/loader" "git.aiterp.net/rpdata/api/internal/loader"
"git.aiterp.net/rpdata/api/internal/store" "git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/changes"
"git.aiterp.net/rpdata/api/models/logs"
"github.com/99designs/gqlgen/handler" "github.com/99designs/gqlgen/handler"
) )
@@ -64,6 +62,8 @@ func logListedChanges() {
log.Printf("\tTag: (%s,%s)", object.Kind, object.Name) log.Printf("\tTag: (%s,%s)", object.Kind, object.Name)
case models.Chapter: case models.Chapter:
log.Printf("\tChapter: %s", object.ID) log.Printf("\tChapter: %s", object.ID)
case models.Comment:
log.Printf("\tComment: %s", object.ID)
} }
} }
} }
@@ -80,6 +80,7 @@ func queryHandler() http.HandlerFunc {
return fmt.Errorf("The server failed to serve your request due to an internal error, it has been logged") return fmt.Errorf("The server failed to serve your request due to an internal error, it has been logged")
}), }),
handler.ComplexityLimit(300),
) )
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
+182
View File
@@ -0,0 +1,182 @@
package graph2
import (
input "git.aiterp.net/rpdata/api/graph2/input"
changes "git.aiterp.net/rpdata/api/models/changes"
channels "git.aiterp.net/rpdata/api/models/channels"
characters "git.aiterp.net/rpdata/api/models/characters"
files "git.aiterp.net/rpdata/api/models/files"
logs "git.aiterp.net/rpdata/api/models/logs"
posts "git.aiterp.net/rpdata/api/models/posts"
stories "git.aiterp.net/rpdata/api/models/stories"
)
func complexity() (cr ComplexityRoot) {
subListComplexity := 25
bigSublistComplexity := 75
hugeSublistComplexity := 100
findComplexity := 100
listComplexity := 200
mutationComplexity := 100
cr.Query.Character = func(childComplexity int, id *string, nick *string) int {
return childComplexity + findComplexity
}
cr.Query.Characters = func(childComplexity int, filter *characters.Filter) int {
return childComplexity + listComplexity
}
cr.Query.Channel = func(childComplexity int, name string) int {
return childComplexity + findComplexity
}
cr.Query.Channels = func(childComplexity int, filter *channels.Filter) int {
return childComplexity + listComplexity
}
cr.Query.Post = func(childComplexity int, id string) int {
return childComplexity + findComplexity
}
cr.Query.Posts = func(childComplexity int, filter *posts.Filter) int {
return childComplexity + listComplexity
}
cr.Query.UnknownNicks = func(childComplexity int, filter *input.UnknownNicksFilter) int {
return childComplexity + listComplexity
}
cr.Query.Log = func(childComplexity int, id string) int {
return childComplexity + findComplexity
}
cr.Query.Logs = func(childComplexity int, filter *logs.Filter) int {
if filter != nil && filter.Open != nil && *filter.Open == true {
return childComplexity + findComplexity
}
return childComplexity + listComplexity
}
cr.Query.Chapter = func(childComplexity int, id string) int {
return childComplexity + findComplexity
}
cr.Query.Comment = func(childComplexity int, id string) int {
return childComplexity + findComplexity
}
cr.Query.Tags = func(childComplexity int) int {
return childComplexity + listComplexity
}
cr.Query.Story = func(childComplexity int, id string) int {
return childComplexity + findComplexity
}
cr.Query.Stories = func(childComplexity int, filter *stories.Filter) int {
return childComplexity + listComplexity
}
cr.Query.File = func(childComplexity int, id string) int {
return childComplexity + findComplexity
}
cr.Query.Files = func(childComplexity int, filter *files.Filter) int {
return childComplexity + listComplexity
}
cr.Query.Changes = func(childComplexity int, filter *changes.Filter) int {
return childComplexity + listComplexity
}
cr.Query.Token = func(childComplexity int) int {
return childComplexity + findComplexity
}
cr.Mutation.AddStory = func(childComplexity int, input input.StoryAddInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.AddStoryTag = func(childComplexity int, input input.StoryTagAddInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.RemoveStoryTag = func(childComplexity int, input input.StoryTagRemoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.EditStory = func(childComplexity int, input input.StoryEditInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.RemoveStory = func(childComplexity int, input input.StoryRemoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.AddChapter = func(childComplexity int, input input.ChapterAddInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.EditChapter = func(childComplexity int, input input.ChapterEditInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.MoveChapter = func(childComplexity int, input input.ChapterMoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.RemoveChapter = func(childComplexity int, input input.ChapterRemoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.AddComment = func(childComplexity int, input input.CommentAddInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.EditComment = func(childComplexity int, input input.CommentEditInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.RemoveComment = func(childComplexity int, input input.CommentRemoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.AddLog = func(childComplexity int, input input.LogAddInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.ImportLog = func(childComplexity int, input input.LogImportInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.EditLog = func(childComplexity int, input input.LogEditInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.RemoveLog = func(childComplexity int, input input.LogRemoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.AddPost = func(childComplexity int, input input.PostAddInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.EditPost = func(childComplexity int, input input.PostEditInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.MovePost = func(childComplexity int, input input.PostMoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.RemovePost = func(childComplexity int, input input.PostRemoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.AddCharacter = func(childComplexity int, input input.CharacterAddInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.AddCharacterNick = func(childComplexity int, input input.CharacterNickInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.RemoveCharacterNick = func(childComplexity int, input input.CharacterNickInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.EditCharacter = func(childComplexity int, input input.CharacterEditInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.RemoveCharacter = func(childComplexity int, input input.CharacterRemoveInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.AddChannel = func(childComplexity int, input input.ChannelAddInput) int {
return childComplexity + mutationComplexity
}
cr.Mutation.EditChannel = func(childComplexity int, input input.ChannelEditInput) int {
return childComplexity + mutationComplexity
}
cr.Log.Characters = func(childComplexity int) int {
return childComplexity + subListComplexity
}
cr.Log.Posts = func(childComplexity int, kinds []string) int {
return childComplexity + hugeSublistComplexity
}
cr.Story.Chapters = func(childComplexity int) int {
return childComplexity + bigSublistComplexity
}
cr.Story.Tags = func(childComplexity int) int {
return childComplexity + subListComplexity
}
cr.Chapter.Comments = func(childComplexity int, limit *int) int {
return childComplexity + subListComplexity
}
return
}
+7
View File
@@ -35,6 +35,13 @@ models:
model: git.aiterp.net/rpdata/api/models/logs.Filter model: git.aiterp.net/rpdata/api/models/logs.Filter
LogImporter: LogImporter:
model: git.aiterp.net/rpdata/api/models.LogImporter model: git.aiterp.net/rpdata/api/models.LogImporter
Comment:
model: git.aiterp.net/rpdata/api/models.Comment
fields:
fictionalDate:
resolver: true
ChapterCommentMode:
model: git.aiterp.net/rpdata/api/models.ChapterCommentMode
Chapter: Chapter:
model: git.aiterp.net/rpdata/api/models.Chapter model: git.aiterp.net/rpdata/api/models.Chapter
fields: fields:
+5
View File
@@ -13,6 +13,7 @@ import (
func New() graphql.ExecutableSchema { func New() graphql.ExecutableSchema {
return NewExecutableSchema(Config{ return NewExecutableSchema(Config{
Resolvers: &rootResolver{}, Resolvers: &rootResolver{},
Complexity: complexity(),
}) })
} }
@@ -34,6 +35,10 @@ func (r *rootResolver) Log() LogResolver {
return &types.LogResolver return &types.LogResolver
} }
func (r *rootResolver) Comment() CommentResolver {
return &types.CommentResolver
}
func (r *rootResolver) Chapter() ChapterResolver { func (r *rootResolver) Chapter() ChapterResolver {
return &types.ChapterResolver return &types.ChapterResolver
} }
+13 -2
View File
@@ -7,6 +7,7 @@ import (
"git.aiterp.net/rpdata/api/models/changekeys" "git.aiterp.net/rpdata/api/models/changekeys"
"git.aiterp.net/rpdata/api/models/changes" "git.aiterp.net/rpdata/api/models/changes"
"git.aiterp.net/rpdata/api/models/comments"
"git.aiterp.net/rpdata/api/internal/auth" "git.aiterp.net/rpdata/api/internal/auth"
"git.aiterp.net/rpdata/api/models/stories" "git.aiterp.net/rpdata/api/models/stories"
@@ -48,7 +49,12 @@ func (r *mutationResolver) AddChapter(ctx context.Context, input input.ChapterAd
return models.Chapter{}, errors.New("Story is not open") return models.Chapter{}, errors.New("Story is not open")
} }
chapter, err := chapters.Add(story, input.Title, author, input.Source, time.Now(), input.FictionalDate) commentMode := models.ChapterCommentModeDisabled
if input.CommentMode != nil {
commentMode = *input.CommentMode
}
chapter, err := chapters.Add(story, input.Title, author, input.Source, time.Now(), input.FictionalDate, commentMode)
if err != nil { if err != nil {
return models.Chapter{}, errors.New("Failed to create chapter: " + err.Error()) return models.Chapter{}, errors.New("Failed to create chapter: " + err.Error())
} }
@@ -117,7 +123,7 @@ func (r *mutationResolver) EditChapter(ctx context.Context, input input.ChapterE
input.FictionalDate = &time.Time{} input.FictionalDate = &time.Time{}
} }
chapter, err = chapters.Edit(chapter, input.Title, input.Source, input.FictionalDate) chapter, err = chapters.Edit(chapter, input.Title, input.Source, input.FictionalDate, input.CommentMode, input.CommentsLocked)
if err != nil { if err != nil {
return models.Chapter{}, errors.New("Failed to edit chapter: " + err.Error()) return models.Chapter{}, errors.New("Failed to edit chapter: " + err.Error())
} }
@@ -150,6 +156,11 @@ func (r *mutationResolver) RemoveChapter(ctx context.Context, input input.Chapte
return models.Chapter{}, errors.New("Failed to remove chapter: " + err.Error()) return models.Chapter{}, errors.New("Failed to remove chapter: " + err.Error())
} }
err = comments.RemoveChapter(chapter)
if err != nil {
return models.Chapter{}, errors.New("Chapter was removed, but comment removal failed: " + err.Error())
}
go func() { go func() {
story, err := stories.FindID(chapter.StoryID) story, err := stories.FindID(chapter.StoryID)
if err != nil { if err != nil {
+175
View File
@@ -0,0 +1,175 @@
package queries
import (
"context"
"errors"
"log"
"time"
"git.aiterp.net/rpdata/api/models/stories"
"git.aiterp.net/rpdata/api/models/characters"
"git.aiterp.net/rpdata/api/models/changekeys"
"git.aiterp.net/rpdata/api/models/changes"
"git.aiterp.net/rpdata/api/graph2/input"
"git.aiterp.net/rpdata/api/internal/auth"
"git.aiterp.net/rpdata/api/models/chapters"
"git.aiterp.net/rpdata/api/models/comments"
"git.aiterp.net/rpdata/api/models"
)
// Queries
func (r *resolver) Comment(ctx context.Context, id string) (models.Comment, error) {
return comments.Find(id)
}
// Mutations
func (r *mutationResolver) AddComment(ctx context.Context, input input.CommentAddInput) (models.Comment, error) {
chapter, err := chapters.FindID(input.ChapterID)
if err != nil {
return models.Comment{}, errors.New("Chapter not found")
}
token := auth.TokenFromContext(ctx)
if !token.Permitted("member", "story.edit") {
return models.Comment{}, errors.New("Unauthorized")
}
if !chapter.CanComment() {
return models.Comment{}, errors.New("Comments are disabled or locked")
}
var characterPtr *models.Character
if input.CharacterID != nil {
character, err := characters.FindID(*input.CharacterID)
if err != nil {
return models.Comment{}, errors.New("Character not found")
} else if character.Author != token.UserID {
return models.Comment{}, errors.New("That is not your character")
}
characterPtr = &character
}
fictionalDate := time.Time{}
if input.FictionalDate != nil {
fictionalDate = *input.FictionalDate
}
subject := ""
if input.Subject != nil {
subject = *input.Subject
}
comment, err := comments.Add(chapter, subject, token.UserID, input.Source, input.CharacterName, characterPtr, time.Now(), fictionalDate)
if err != nil {
return models.Comment{}, errors.New("Failed to add comment: " + err.Error())
}
go func() {
story, err := stories.FindID(chapter.StoryID)
if err != nil {
log.Println("WARNING: Couldn't log comment change:", err)
return
}
changes.Submit("Comment", "add", token.UserID, true, changekeys.Many(comment, chapter, models.Story{ID: chapter.StoryID}), comment, chapter, story)
}()
return comment, nil
}
func (r *mutationResolver) EditComment(ctx context.Context, input input.CommentEditInput) (models.Comment, error) {
comment, err := comments.Find(input.CommentID)
if err != nil {
return models.Comment{}, errors.New("Comment not found")
}
token := auth.TokenFromContext(ctx)
if !token.PermittedUser(comment.Author, "member", "story.edit") {
return models.Comment{}, errors.New("You cannot edit this comment")
}
chapter, err := chapters.FindID(comment.ChapterID)
if err != nil {
return models.Comment{}, errors.New("Comment's chapter not found")
}
if !chapter.CanComment() {
return models.Comment{}, errors.New("Comments are disabled or locked")
}
if input.ClearFictionalDate != nil && *input.ClearFictionalDate == true {
input.FictionalDate = &time.Time{}
}
if input.CharacterID != nil && *input.CharacterID != "" {
character, err := characters.FindID(*input.CharacterID)
if err != nil {
return models.Comment{}, errors.New("Character not found")
} else if character.Author != token.UserID {
return models.Comment{}, errors.New("That is not your character")
}
}
comment, err = comments.Edit(comment, input.Source, input.CharacterName, input.CharacterID, input.Subject, input.FictionalDate)
if err != nil {
return models.Comment{}, errors.New("Could not post comment: " + err.Error())
}
go func() {
story, err := stories.FindID(chapter.StoryID)
if err != nil {
log.Println("WARNING: Couldn't log comment change:", err)
return
}
changes.Submit("Comment", "edit", token.UserID, true, changekeys.Many(comment, chapter, models.Story{ID: chapter.StoryID}), comment, chapter, story)
}()
return comment, nil
}
func (r *mutationResolver) RemoveComment(ctx context.Context, input input.CommentRemoveInput) (models.Comment, error) {
comment, err := comments.Find(input.CommentID)
if err != nil {
return models.Comment{}, errors.New("Comment not found")
}
token := auth.TokenFromContext(ctx)
if !token.PermittedUser(comment.Author, "member", "story.edit") {
return models.Comment{}, errors.New("You cannot remove this comment")
}
chapter, err := chapters.FindID(comment.ChapterID)
if err != nil {
return models.Comment{}, errors.New("Comment's chapter not found")
}
if !chapter.CanComment() {
return models.Comment{}, errors.New("Comments are disabled or locked")
}
err = comments.Remove(comment)
if err != nil {
return models.Comment{}, errors.New("Failed to remove comment: " + err.Error())
}
go func() {
story, err := stories.FindID(chapter.StoryID)
if err != nil {
log.Println("WARNING: Couldn't log comment change:", err)
return
}
changes.Submit("Comment", "remove", token.UserID, true, changekeys.Many(comment, chapter, models.Story{ID: chapter.StoryID}), comment, chapter, story)
}()
return comment, nil
}
+14
View File
@@ -41,6 +41,10 @@ type Query {
chapter(id: String!): Chapter! chapter(id: String!): Chapter!
# Find comment by ID
comment(id: String!): Comment!
# Find all distinct tags used in stories # Find all distinct tags used in stories
tags: [Tag!]! tags: [Tag!]!
@@ -97,6 +101,16 @@ type Mutation {
removeChapter(input: ChapterRemoveInput!): Chapter! removeChapter(input: ChapterRemoveInput!): Chapter!
"Add a comment to a chapter."
addComment(input: CommentAddInput!): Comment!
"Edit a comment in a chapter."
editComment(input: CommentEditInput!): Comment!
"Remove a comemnt in a chapter."
removeComment(input: CommentRemoveInput!): Comment!
# Add a new log # Add a new log
addLog(input: LogAddInput!): Log! addLog(input: LogAddInput!): Log!
+1 -1
View File
@@ -27,7 +27,7 @@ type Change {
objects: [ChangeObject!]! objects: [ChangeObject!]!
} }
union ChangeObject = Character | Channel | Log | Post | Story | Tag | Chapter union ChangeObject = Character | Channel | Log | Post | Story | Tag | Chapter | Comment
""" """
A change key is a key associated with a change, used for filtering A change key is a key associated with a change, used for filtering
+38
View File
@@ -20,6 +20,35 @@ type Chapter {
# The date of edit. # The date of edit.
editedDate: Date! editedDate: Date!
"The comment mode."
commentMode: ChapterCommentMode!
"Whether the author has locket comments."
commentsLocked: Boolean!
"A shorthand for checking whether a logged-in user can comment on this chapter."
canComment: Boolean!
"Get all chapter comments."
comments(limit: Int): [Comment!]!
}
"""
The possibles modes of comments for a chapter.
"""
enum ChapterCommentMode {
"Comments are disabled and hidden."
Disabled
"Comments should be shown as typical news article comments."
Article
"Comments should be shown in a compact form suitable for many small messages."
Chat
"Comments are going to be shown as omni-tool messages."
Message
} }
# Input for addChapter mutation # Input for addChapter mutation
@@ -41,6 +70,9 @@ input ChapterAddInput {
# Optionally, assign a fictional date for the chapter # Optionally, assign a fictional date for the chapter
fictionalDate: Date fictionalDate: Date
"The comment mode for the chapter."
commentMode: ChapterCommentMode
} }
# Input for editChapter mutation # Input for editChapter mutation
@@ -59,6 +91,12 @@ input ChapterEditInput {
# Remove the fictional date for a chapter # Remove the fictional date for a chapter
clearFictionalDate: Boolean clearFictionalDate: Boolean
"Set the comment mode for the chapter"
commentMode: ChapterCommentMode
"Set whether the chapter comments are locked"
commentsLocked: Boolean
} }
# Input for moveChapter mutation # Input for moveChapter mutation
+91
View File
@@ -0,0 +1,91 @@
"""
A Comment represents a comment to a story chapter.
"""
type Comment {
"A unique ID of the change."
id: String!
"subject"
subject: String!
"The chapter ID the comment belongs to."
chapterId: String!
"The comment's author."
author: String!
"The displayed name of the character. This may be the same as character.name, but it does not need to be."
characterName: String!
"The character associated with the comment."
character: Character
"The fictional (IC) date of the comment."
fictionalDate: Date
"The date of creation."
createdDate: Date!
"The date of the last edit."
editedDate: Date!
"The markdown source of the comment."
source: String!
}
"""
Input for the addComment mutation
"""
input CommentAddInput {
"What chapter to comment on."
chapterId: String!
"The markdown source of the comment."
source: String!
"The name of the character associated with this comment."
characterName: String!
"Optioanlly, a character ID."
characterId: String
"Optional in-universe date of the comment."
fictionalDate: Date
"Optional subject to add to the comment, only shown in some modes."
subject: String
}
"""
Input for the editComment mutation
"""
input CommentEditInput {
"What comment to edit."
commentId: String!
"Change the markdown source."
source: String
"Change the name of the character associated with this comment."
characterName: String
"Change the character assocaited with this comment."
characterId: String
"Change the optional in-universe date of the comment."
fictionalDate: Date
"Set to clear the fictional date."
clearFictionalDate: Boolean
"Change the optional subject for the comment, only shown in some modes."
subject: String
}
"""
Input for the removeComment mutation
"""
input CommentRemoveInput {
"What comment to edit."
commentId: String!
}
+19
View File
@@ -2,9 +2,11 @@ package types
import ( import (
"context" "context"
"errors"
"time" "time"
"git.aiterp.net/rpdata/api/models" "git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/comments"
) )
type chapterResolver struct{} type chapterResolver struct{}
@@ -17,5 +19,22 @@ func (r *chapterResolver) FictionalDate(ctx context.Context, chapter *models.Cha
return &chapter.FictionalDate, nil return &chapter.FictionalDate, nil
} }
func (r *chapterResolver) Comments(ctx context.Context, chapter *models.Chapter, limit *int) ([]models.Comment, error) {
limitValue := 0
if limit != nil {
if *limit < 0 {
return nil, errors.New("Limit cannot be negative")
}
limitValue = *limit
}
if !chapter.CommentMode.IsEnabled() {
return nil, nil
}
return comments.ListChapterID(chapter.ID, limitValue)
}
// ChapterResolver is a resolver // ChapterResolver is a resolver
var ChapterResolver chapterResolver var ChapterResolver chapterResolver
+41
View File
@@ -0,0 +1,41 @@
package types
import (
"context"
"errors"
"time"
"git.aiterp.net/rpdata/api/internal/loader"
"git.aiterp.net/rpdata/api/models"
)
type commentResolver struct{}
func (r *commentResolver) Character(ctx context.Context, obj *models.Comment) (*models.Character, error) {
if obj.CharacterID == "" {
return nil, nil
}
loader := loader.FromContext(ctx)
if loader == nil {
return nil, errors.New("no loader")
}
character, err := loader.Character("id", obj.CharacterID)
if err != nil {
return nil, err
}
return &character, nil
}
func (r *commentResolver) FictionalDate(ctx context.Context, obj *models.Comment) (*time.Time, error) {
if obj.FictionalDate.IsZero() {
return nil, nil
}
return &obj.FictionalDate, nil
}
// CommentResolver is a resolver
var CommentResolver commentResolver
+3 -1
View File
@@ -24,12 +24,14 @@ const (
ChangeModelTag ChangeModel = "Tag" ChangeModelTag ChangeModel = "Tag"
// ChangeModelChapter is a value of ChangeModel // ChangeModelChapter is a value of ChangeModel
ChangeModelChapter ChangeModel = "Chapter" ChangeModelChapter ChangeModel = "Chapter"
// ChangeModelComment is a value of ChangeModel
ChangeModelComment ChangeModel = "Comment"
) )
// IsValid returns true if the underlying string is one of the correct values. // IsValid returns true if the underlying string is one of the correct values.
func (e ChangeModel) IsValid() bool { func (e ChangeModel) IsValid() bool {
switch e { switch e {
case ChangeModelCharacter, ChangeModelChannel, ChangeModelLog, ChangeModelPost, ChangeModelStory, ChangeModelTag, ChangeModelChapter: case ChangeModelCharacter, ChangeModelChannel, ChangeModelLog, ChangeModelPost, ChangeModelStory, ChangeModelTag, ChangeModelChapter, ChangeModelComment:
return true return true
} }
return false return false
+5 -1
View File
@@ -19,6 +19,7 @@ type Change struct {
Stories []Story `bson:"stories"` Stories []Story `bson:"stories"`
Tags []Tag `bson:"tags"` Tags []Tag `bson:"tags"`
Chapters []Chapter `bson:"chapters"` Chapters []Chapter `bson:"chapters"`
Comments []Comment `bson:"comments"`
} }
// ChangeKey is a key for a change that can be used when subscribing to them. // ChangeKey is a key for a change that can be used when subscribing to them.
@@ -29,7 +30,7 @@ type ChangeKey struct {
// Objects makes a combined, mixed array of all the models stored in this change. // Objects makes a combined, mixed array of all the models stored in this change.
func (change *Change) Objects() []interface{} { func (change *Change) Objects() []interface{} {
data := make([]interface{}, 0, len(change.Logs)+len(change.Characters)+len(change.Channels)+len(change.Posts)+len(change.Stories)+len(change.Tags)+len(change.Chapters)) data := make([]interface{}, 0, len(change.Logs)+len(change.Characters)+len(change.Channels)+len(change.Posts)+len(change.Stories)+len(change.Tags)+len(change.Chapters)+len(change.Comments))
for _, log := range change.Logs { for _, log := range change.Logs {
data = append(data, log) data = append(data, log)
@@ -52,6 +53,9 @@ func (change *Change) Objects() []interface{} {
for _, chapter := range change.Chapters { for _, chapter := range change.Chapters {
data = append(data, chapter) data = append(data, chapter)
} }
for _, comment := range change.Comments {
data = append(data, comment)
}
return data return data
} }
+2
View File
@@ -22,6 +22,7 @@ func TestMany(t *testing.T) {
}, },
}, },
models.Post{ID: "P5"}, models.Post{ID: "P5"},
models.Comment{ID: "SCC1234"},
} }
expectations := []models.ChangeKey{ expectations := []models.ChangeKey{
@@ -33,6 +34,7 @@ func TestMany(t *testing.T) {
{Model: "Character", ID: "C17"}, {Model: "Character", ID: "C17"},
{Model: "Channel", ID: "#Stuff"}, {Model: "Channel", ID: "#Stuff"},
{Model: "Post", ID: "P5"}, {Model: "Post", ID: "P5"},
{Model: "Comment", ID: "SCC1234"},
} }
results := changekeys.Many(data...) results := changekeys.Many(data...)
+5
View File
@@ -28,6 +28,11 @@ func TestOne(t *testing.T) {
models.Log{ID: "2018-10-23_210303325_RedrockAgency", ShortID: "L807"}, models.Log{ID: "2018-10-23_210303325_RedrockAgency", ShortID: "L807"},
models.ChangeKey{Model: "Log", ID: "2018-10-23_210303325_RedrockAgency"}, models.ChangeKey{Model: "Log", ID: "2018-10-23_210303325_RedrockAgency"},
}, },
{
"Comment_SCC1234",
models.Comment{ID: "SCC1234"},
models.ChangeKey{Model: "Comment", ID: "SCC1234"},
},
} }
for _, row := range table { for _, row := range table {
+4
View File
@@ -81,6 +81,10 @@ func Submit(model, op, author string, listed bool, keys []models.ChangeKey, obje
change.Chapters = append(change.Chapters, object) change.Chapters = append(change.Chapters, object)
case []models.Chapter: case []models.Chapter:
change.Chapters = append(change.Chapters, object...) change.Chapters = append(change.Chapters, object...)
case models.Comment:
change.Comments = append(change.Comments, object)
case []models.Comment:
change.Comments = append(change.Comments, object...)
default: default:
log.Printf("Warning: unrecognized object in change: %#+v", object) log.Printf("Warning: unrecognized object in change: %#+v", object)
} }
+57
View File
@@ -0,0 +1,57 @@
package models
import (
"fmt"
"io"
)
// ChapterCommentMode represents the kind of tags.
type ChapterCommentMode string
const (
// ChapterCommentModeDisabled is a chapter comment mode, see GraphQL documentation.
ChapterCommentModeDisabled ChapterCommentMode = "Disabled"
// ChapterCommentModeArticle is a chapter comment mode, see GraphQL documentation.
ChapterCommentModeArticle ChapterCommentMode = "Article"
// ChapterCommentModeChat is a chapter comment mode, see GraphQL documentation.
ChapterCommentModeChat ChapterCommentMode = "Chat"
// ChapterCommentModeMessage is a chapter comment mode, see GraphQL documentation.
ChapterCommentModeMessage ChapterCommentMode = "Message"
)
// UnmarshalGQL unmarshals
func (e *ChapterCommentMode) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = ChapterCommentMode(str)
switch *e {
case ChapterCommentModeDisabled, ChapterCommentModeArticle, ChapterCommentModeChat, ChapterCommentModeMessage:
return nil
default:
return fmt.Errorf("%s is not a valid ChapterCommentMode", str)
}
}
// IsEnabled returns true if comments are enabled.
func (e ChapterCommentMode) IsEnabled() bool {
return e != ChapterCommentModeDisabled && len(e) != 0
}
// MarshalGQL turns it into a JSON string
func (e ChapterCommentMode) MarshalGQL(w io.Writer) {
// Backwards compatibility: Empty value means disabled.
if len(e) == 0 {
w.Write(disabledChapterCommentModeBytes)
return
}
fmt.Fprint(w, "\""+string(e)+"\"")
}
var disabledChapterCommentModeBytes = []byte(`"Disabled"`)
+7
View File
@@ -12,4 +12,11 @@ type Chapter struct {
CreatedDate time.Time `bson:"createdDate"` CreatedDate time.Time `bson:"createdDate"`
FictionalDate time.Time `bson:"fictionalDate,omitempty"` FictionalDate time.Time `bson:"fictionalDate,omitempty"`
EditedDate time.Time `bson:"editedDate"` EditedDate time.Time `bson:"editedDate"`
CommentMode ChapterCommentMode `bson:"commentMode"`
CommentsLocked bool `bson:"commentsLocked"`
}
// CanComment returns true if the chapter can be commented to.
func (chapter *Chapter) CanComment() bool {
return !chapter.CommentsLocked && chapter.CommentMode.IsEnabled()
} }
+3 -1
View File
@@ -8,7 +8,7 @@ import (
) )
// Add adds a new chapter. // Add adds a new chapter.
func Add(story models.Story, title, author, source string, createdDate time.Time, finctionalDate *time.Time) (models.Chapter, error) { func Add(story models.Story, title, author, source string, createdDate time.Time, finctionalDate *time.Time, commentMode models.ChapterCommentMode) (models.Chapter, error) {
chapter := models.Chapter{ chapter := models.Chapter{
ID: makeChapterID(), ID: makeChapterID(),
StoryID: story.ID, StoryID: story.ID,
@@ -17,6 +17,8 @@ func Add(story models.Story, title, author, source string, createdDate time.Time
Source: source, Source: source,
CreatedDate: createdDate, CreatedDate: createdDate,
EditedDate: createdDate, EditedDate: createdDate,
CommentMode: commentMode,
CommentsLocked: false,
} }
if finctionalDate != nil { if finctionalDate != nil {
+9 -1
View File
@@ -9,7 +9,7 @@ import (
// Edit edits a chapter, and updates EditedDate. While many Edit functions cheat if there's nothing to // Edit edits a chapter, and updates EditedDate. While many Edit functions cheat if there's nothing to
// change, this functill will due to EditedDate. // change, this functill will due to EditedDate.
func Edit(chapter models.Chapter, title, source *string, fictionalDate *time.Time) (models.Chapter, error) { func Edit(chapter models.Chapter, title, source *string, fictionalDate *time.Time, commentMode *models.ChapterCommentMode, commentsLocked *bool) (models.Chapter, error) {
now := time.Now() now := time.Now()
changes := bson.M{"editedDate": now} changes := bson.M{"editedDate": now}
@@ -28,6 +28,14 @@ func Edit(chapter models.Chapter, title, source *string, fictionalDate *time.Tim
changes["fictionalDate"] = *fictionalDate changes["fictionalDate"] = *fictionalDate
edited.FictionalDate = *fictionalDate edited.FictionalDate = *fictionalDate
} }
if commentMode != nil && *commentMode != chapter.CommentMode {
changes["commentMode"] = *commentMode
edited.CommentMode = *commentMode
}
if commentsLocked != nil && *commentsLocked != chapter.CommentsLocked {
changes["commentsLocked"] = *commentsLocked
edited.CommentsLocked = *commentsLocked
}
err := collection.UpdateId(chapter.ID, bson.M{"$set": changes}) err := collection.UpdateId(chapter.ID, bson.M{"$set": changes})
if err != nil { if err != nil {
+17
View File
@@ -0,0 +1,17 @@
package models
import "time"
// A Comment is a comment on a chapter.
type Comment struct {
ID string `bson:"_id"`
ChapterID string `bson:"chapterId"`
Subject string `bson:"subject"`
Author string `bson:"author"`
CharacterName string `bson:"characterName"`
CharacterID string `bson:"characterId"`
FictionalDate time.Time `bson:"fictionalDate"`
CreatedDate time.Time `bson:"createdDate"`
EditedDate time.Time `bson:"editeddDate"`
Source string `bson:"source"`
}
+35
View File
@@ -0,0 +1,35 @@
package comments
import (
"time"
"git.aiterp.net/rpdata/api/models"
)
// Add adds a comment.
func Add(chapter models.Chapter, subject, author, source, characterName string, character *models.Character, createdDate time.Time, fictionalDate time.Time) (models.Comment, error) {
characterID := ""
if character != nil {
characterID = character.ID
}
comment := models.Comment{
ID: makeCommentID(),
ChapterID: chapter.ID,
Subject: subject,
Author: author,
CharacterName: characterName,
CharacterID: characterID,
FictionalDate: fictionalDate,
CreatedDate: createdDate,
EditedDate: createdDate,
Source: source,
}
err := collection.Insert(comment)
if err != nil {
return models.Comment{}, err
}
return comment, nil
}
+66
View File
@@ -0,0 +1,66 @@
package comments
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.Comment, error) {
comment := models.Comment{}
err := collection.Find(query).One(&comment)
return comment, err
}
func list(query interface{}, limit int) ([]models.Comment, error) {
allocSize := 32
if limit >= 0 {
allocSize = limit
} else {
limit = 0
}
comments := make([]models.Comment, 0, allocSize)
err := collection.Find(query).Sort("createdDate").Limit(limit).All(&comments)
if err != nil {
return nil, err
}
return comments, nil
}
func makeCommentID() string {
result := "SCC"
offset := 0
data := make([]byte, 48)
rand.Read(data)
for len(result) < 32 {
result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36)
offset += 8
if offset >= 48 {
rand.Read(data)
offset = 0
}
}
return result[:32]
}
func init() {
store.HandleInit(func(db *mgo.Database) {
collection = db.C("story.comments")
collection.EnsureIndexKey("chapterId")
collection.EnsureIndexKey("author")
collection.EnsureIndexKey("createdDate")
})
}
+44
View File
@@ -0,0 +1,44 @@
package comments
import (
"time"
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo/bson"
)
// Edit edits a comment.
func Edit(comment models.Comment, source, characterName, characterID, subject *string, fictionalDate *time.Time) (models.Comment, error) {
changes := make(bson.M, 6)
comment.EditedDate = time.Now()
changes["editedDate"] = comment.EditedDate
if source != nil {
comment.Source = *source
changes["source"] = *source
}
if characterName != nil {
comment.CharacterName = *characterName
changes["characterName"] = *characterName
}
if characterID != nil {
comment.CharacterID = *characterID
changes["characterId"] = *characterID
}
if subject != nil {
comment.Subject = *subject
changes["subject"] = *subject
}
if fictionalDate != nil {
comment.FictionalDate = *fictionalDate
changes["fictionalDate"] = *fictionalDate
}
err := collection.UpdateId(comment.ID, bson.M{"$set": changes})
if err != nil {
return models.Comment{}, err
}
return comment, nil
}
+11
View File
@@ -0,0 +1,11 @@
package comments
import (
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo/bson"
)
// Find finds a comment by ID.
func Find(id string) (models.Comment, error) {
return find(bson.M{"_id": id})
}
+11
View File
@@ -0,0 +1,11 @@
package comments
import (
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo/bson"
)
// ListChapterID lists all comments by chapter-ID
func ListChapterID(chapterID string, limit int) ([]models.Comment, error) {
return list(bson.M{"chapterId": chapterID}, limit)
}
+17
View File
@@ -0,0 +1,17 @@
package comments
import (
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo/bson"
)
// Remove removes one comment.
func Remove(comment models.Comment) error {
return collection.RemoveId(comment.ID)
}
// RemoveChapter removes all comments for the given chapter.
func RemoveChapter(chapter models.Chapter) error {
_, err := collection.RemoveAll(bson.M{"chapterId": chapter.ID})
return err
}