From a4034b0faa4751fa73f88f77ac35763c76f8a9e7 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Mon, 17 Sep 2018 20:11:57 +0200 Subject: [PATCH] graph2: Added Chapter mutations --- graph2/graph.go | 4 +++ graph2/queries/chapter.go | 61 +++++++++++++++++++++++++++++++++ graph2/queries/resolver.go | 4 +++ graph2/schema/root.gql | 12 +++++++ graph2/schema/types/Chapter.gql | 6 ++++ internal/auth/permissions.go | 1 + models/chapters/add.go | 38 ++++++++++++++++++++ models/chapters/db.go | 25 ++++++++++++++ models/chapters/edit.go | 38 ++++++++++++++++++++ models/chapters/remove.go | 12 +++++++ 10 files changed, 201 insertions(+) create mode 100644 models/chapters/add.go create mode 100644 models/chapters/edit.go create mode 100644 models/chapters/remove.go diff --git a/graph2/graph.go b/graph2/graph.go index 684a7c0..4e1623d 100644 --- a/graph2/graph.go +++ b/graph2/graph.go @@ -22,6 +22,10 @@ func (r *rootResolver) Query() QueryResolver { return &queries.Resolver } +func (r *rootResolver) Mutation() MutationResolver { + return queries.MutationResolver +} + func (r *rootResolver) Log() LogResolver { return &types.LogResolver } diff --git a/graph2/queries/chapter.go b/graph2/queries/chapter.go index b9db775..3a36390 100644 --- a/graph2/queries/chapter.go +++ b/graph2/queries/chapter.go @@ -2,7 +2,13 @@ package queries import ( "context" + "errors" + "time" + "git.aiterp.net/rpdata/api/internal/auth" + "git.aiterp.net/rpdata/api/models/stories" + + "git.aiterp.net/rpdata/api/graph2/input" "git.aiterp.net/rpdata/api/models" "git.aiterp.net/rpdata/api/models/chapters" ) @@ -10,3 +16,58 @@ import ( func (r *resolver) Chapter(ctx context.Context, id string) (models.Chapter, error) { return chapters.FindID(id) } + +func (r *mutationResolver) AddChapter(ctx context.Context, input input.ChapterAddInput) (models.Chapter, error) { + story, err := stories.FindID(input.StoryID) + if err != nil { + return models.Chapter{}, errors.New("Story not found") + } + + token := auth.TokenFromContext(ctx) + if !token.Authenticated() { + return models.Chapter{}, errors.New("Unauthorized") + } + + author := token.UserID + if input.Author != nil && *input.Author != author { + if !token.Permitted("story.add") { + return models.Chapter{}, errors.New("False pretender") + } + + author = *input.Author + } + + if !story.Open && story.Author != author { + return models.Chapter{}, errors.New("Story is not open") + } + + return chapters.Add(story, input.Title, author, input.Source, time.Now(), input.FictionalDate) +} + +func (r *mutationResolver) EditChapter(ctx context.Context, input input.ChapterEditInput) (models.Chapter, error) { + chapter, err := chapters.FindID(input.ID) + if err != nil { + return models.Chapter{}, errors.New("Chapter not found") + } + + token := auth.TokenFromContext(ctx) + if !token.Authenticated() || !token.PermittedUser(chapter.Author, "member", "story.edit") { + return models.Chapter{}, errors.New("Unauthorized") + } + + return chapters.Edit(chapter, input.Title, input.Source, input.FictionalDate) +} + +func (r *mutationResolver) RemoveChapter(ctx context.Context, input input.ChapterRemoveInput) (models.Chapter, error) { + chapter, err := chapters.FindID(input.ID) + if err != nil { + return models.Chapter{}, errors.New("Chapter not found") + } + + token := auth.TokenFromContext(ctx) + if !token.Authenticated() || !token.PermittedUser(chapter.Author, "member", "story.remove") { + return models.Chapter{}, errors.New("Unauthorized") + } + + return chapters.Remove(chapter) +} diff --git a/graph2/queries/resolver.go b/graph2/queries/resolver.go index 47e66a9..8403a82 100644 --- a/graph2/queries/resolver.go +++ b/graph2/queries/resolver.go @@ -1,6 +1,10 @@ package queries type resolver struct{} +type mutationResolver struct{} // Resolver has all the queries var Resolver resolver + +// MutationResolver brings the mutagens. +var MutationResolver *mutationResolver diff --git a/graph2/schema/root.gql b/graph2/schema/root.gql index c9497a7..0b51517 100644 --- a/graph2/schema/root.gql +++ b/graph2/schema/root.gql @@ -1,5 +1,6 @@ schema { query: Query + mutation: Mutation } type Query { @@ -57,6 +58,17 @@ type Query { token: Token! } +type Mutation { + # Add a chapter to a story + addChapter(input: ChapterAddInput!): Chapter! + + # Edit a chapter + editChapter(input: ChapterEditInput!): Chapter! + + # Remove a chapter + removeChapter(input: ChapterRemoveInput!): Chapter! +} + # A Date represents a RFC3339 encoded date with up to millisecond precision. scalar Date diff --git a/graph2/schema/types/Chapter.gql b/graph2/schema/types/Chapter.gql index f932ac7..99755aa 100644 --- a/graph2/schema/types/Chapter.gql +++ b/graph2/schema/types/Chapter.gql @@ -56,4 +56,10 @@ input ChapterEditInput { # Set the fictional date for a chapter fictionalDate: Date +} + +# Input for removeChapter mutation +input ChapterRemoveInput { + # The chapter to remove + id: String! } \ No newline at end of file diff --git a/internal/auth/permissions.go b/internal/auth/permissions.go index ea13482..2954e25 100644 --- a/internal/auth/permissions.go +++ b/internal/auth/permissions.go @@ -18,6 +18,7 @@ func AllPermissions() map[string]string { "post.edit": "Can edit posts", "post.move": "Can move posts", "post.remove": "Can remove posts", + "story.add": "Can add non-owned stories", "story.edit": "Can edit non-owned stories", "story.remove": "Can remove non-owned stories", "story.unlisted": "Can view non-owned unlisted stories", diff --git a/models/chapters/add.go b/models/chapters/add.go new file mode 100644 index 0000000..87e7f12 --- /dev/null +++ b/models/chapters/add.go @@ -0,0 +1,38 @@ +package chapters + +import ( + "time" + + "git.aiterp.net/rpdata/api/models" + "github.com/globalsign/mgo/bson" +) + +// Add adds a new chapter. +func Add(story models.Story, title, author, source string, createdDate time.Time, finctionalDate *time.Time) (models.Chapter, error) { + chapter := models.Chapter{ + ID: makeChapterID(), + StoryID: story.ID, + Title: title, + Author: author, + Source: source, + CreatedDate: createdDate, + EditedDate: createdDate, + } + + if finctionalDate != nil { + chapter.FictionalDate = *finctionalDate + } + + err := collection.Insert(chapter) + if err != nil { + return models.Chapter{}, err + } + + if createdDate.After(story.UpdatedDate) { + if err := storyCollection.UpdateId(story.ID, bson.M{"$set": bson.M{"updatedDate": createdDate}}); err == nil { + story.UpdatedDate = createdDate + } + } + + return chapter, nil +} diff --git a/models/chapters/db.go b/models/chapters/db.go index c111baf..71d791a 100644 --- a/models/chapters/db.go +++ b/models/chapters/db.go @@ -1,12 +1,17 @@ package chapters 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 +var storyCollection *mgo.Collection func find(query interface{}) (models.Chapter, error) { chapter := models.Chapter{} @@ -25,9 +30,29 @@ func list(query interface{}) ([]models.Chapter, error) { return chapters, nil } +func makeChapterID() string { + result := "SC" + offset := 0 + data := make([]byte, 32) + + rand.Read(data) + for len(result) < 24 { + result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36) + offset += 8 + + if offset >= 32 { + rand.Read(data) + offset = 0 + } + } + + return result[:24] +} + func init() { store.HandleInit(func(db *mgo.Database) { collection = db.C("story.chapters") + storyCollection = db.C("story.stories") collection.EnsureIndexKey("storyId") collection.EnsureIndexKey("author") diff --git a/models/chapters/edit.go b/models/chapters/edit.go new file mode 100644 index 0000000..f3816ce --- /dev/null +++ b/models/chapters/edit.go @@ -0,0 +1,38 @@ +package chapters + +import ( + "time" + + "git.aiterp.net/rpdata/api/models" + "github.com/globalsign/mgo/bson" +) + +// Edit edits a chapter, and updates EditedDate. While many Edit functions cheat if there's nothing to +// change, this functill will due to EditedDate. +func Edit(chapter models.Chapter, title, source *string, fictionalDate *time.Time) (models.Chapter, error) { + now := time.Now() + changes := bson.M{"editedDate": now} + + edited := chapter + edited.EditedDate = now + + if title != nil && *title != chapter.Title { + changes["title"] = *title + edited.Title = *title + } + if source != nil && *source != chapter.Source { + changes["source"] = *source + edited.Source = *source + } + if fictionalDate != nil && !fictionalDate.Equal(chapter.FictionalDate) { + changes["fictionalDate"] = *fictionalDate + edited.FictionalDate = *fictionalDate + } + + err := collection.UpdateId(chapter.ID, bson.M{"$set": changes}) + if err != nil { + return chapter, err + } + + return edited, nil +} diff --git a/models/chapters/remove.go b/models/chapters/remove.go new file mode 100644 index 0000000..a315684 --- /dev/null +++ b/models/chapters/remove.go @@ -0,0 +1,12 @@ +package chapters + +import "git.aiterp.net/rpdata/api/models" + +// Remove removes a chapter. +func Remove(chapter models.Chapter) (models.Chapter, error) { + if err := collection.RemoveId(chapter.ID); err != nil { + return models.Chapter{}, err + } + + return chapter, nil +}