Gisle Aune
5 years ago
12 changed files with 493 additions and 4 deletions
-
12database/mongodb/db.go
-
208database/mongodb/stories.go
-
5internal/generate/id.go
-
13models/chapter.go
-
15models/comment.go
-
21models/story.go
-
14repositories/chapter.go
-
14repositories/comment.go
-
6repositories/repository.go
-
16repositories/story.go
-
15services/changes.go
-
152services/stories.go
@ -0,0 +1,208 @@ |
|||||
|
package mongodb |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"git.aiterp.net/rpdata/api/internal/generate" |
||||
|
"git.aiterp.net/rpdata/api/models" |
||||
|
"git.aiterp.net/rpdata/api/repositories" |
||||
|
"github.com/globalsign/mgo" |
||||
|
"github.com/globalsign/mgo/bson" |
||||
|
"log" |
||||
|
) |
||||
|
|
||||
|
type storyRepository struct { |
||||
|
stories *mgo.Collection |
||||
|
chapters *mgo.Collection |
||||
|
comments *mgo.Collection |
||||
|
} |
||||
|
|
||||
|
func newStoryRepository(db *mgo.Database) (repositories.StoryRepository, error) { |
||||
|
collection := db.C("story.stories") |
||||
|
|
||||
|
err := collection.EnsureIndexKey("tags") |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
err = collection.EnsureIndexKey("author") |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
err = collection.EnsureIndexKey("updatedDate") |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
err = collection.EnsureIndexKey("fictionalDate") |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
err = collection.EnsureIndexKey("listed") |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return &storyRepository{ |
||||
|
stories: collection, |
||||
|
chapters: db.C("story.chapters"), |
||||
|
comments: db.C("story.comments"), |
||||
|
}, nil |
||||
|
} |
||||
|
|
||||
|
func (r *storyRepository) Find(ctx context.Context, id string) (*models.Story, error) { |
||||
|
story := new(models.Story) |
||||
|
err := r.stories.FindId(id).One(story) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return story, nil |
||||
|
} |
||||
|
|
||||
|
func (r *storyRepository) List(ctx context.Context, filter models.StoryFilter) ([]*models.Story, error) { |
||||
|
query := bson.M{} |
||||
|
if filter.Author != nil { |
||||
|
query["author"] = *filter.Author |
||||
|
} |
||||
|
if filter.Category != nil { |
||||
|
query["category"] = *filter.Category |
||||
|
} |
||||
|
if filter.Open != nil { |
||||
|
query["open"] = *filter.Open |
||||
|
} |
||||
|
if len(filter.Tags) > 0 { |
||||
|
query["tags"] = bson.M{"$all": filter.Tags} |
||||
|
} |
||||
|
if filter.Unlisted != nil { |
||||
|
query["listed"] = *filter.Unlisted |
||||
|
} |
||||
|
if !filter.EarliestFictionalDate.IsZero() && !filter.LatestFictionalDate.IsZero() { |
||||
|
query["fictionalDate"] = bson.M{ |
||||
|
"$gte": filter.EarliestFictionalDate, |
||||
|
"$lte": filter.LatestFictionalDate, |
||||
|
} |
||||
|
} else if !filter.EarliestFictionalDate.IsZero() { |
||||
|
query["fictionalDate"] = bson.M{ |
||||
|
"$gte": filter.EarliestFictionalDate, |
||||
|
} |
||||
|
} else if !filter.LatestFictionalDate.IsZero() { |
||||
|
query["fictionalDate"] = bson.M{ |
||||
|
"$lte": filter.LatestFictionalDate, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
stories := make([]*models.Story, 0, 32) |
||||
|
err := r.stories.Find(query).Sort("-updatedDate ").Limit(filter.Limit).All(&stories) |
||||
|
if err != nil { |
||||
|
if err == mgo.ErrNotFound { |
||||
|
return stories, nil |
||||
|
} |
||||
|
|
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return stories, nil |
||||
|
} |
||||
|
|
||||
|
func (r *storyRepository) Insert(ctx context.Context, story models.Story) (*models.Story, error) { |
||||
|
story.ID = generate.StoryID() |
||||
|
|
||||
|
err := r.stories.Insert(story) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return &story, nil |
||||
|
} |
||||
|
|
||||
|
func (r *storyRepository) Update(ctx context.Context, story models.Story, update models.StoryUpdate) (*models.Story, error) { |
||||
|
updateBson := bson.M{} |
||||
|
if update.Name != nil { |
||||
|
updateBson["name"] = *update.Name |
||||
|
story.Name = *update.Name |
||||
|
} |
||||
|
if update.Open != nil { |
||||
|
updateBson["open"] = *update.Open |
||||
|
story.Open = *update.Open |
||||
|
} |
||||
|
if update.Category != nil { |
||||
|
updateBson["category"] = *update.Category |
||||
|
story.Category = *update.Category |
||||
|
} |
||||
|
if update.Author != nil { |
||||
|
updateBson["author"] = *update.Author |
||||
|
story.Author = *update.Author |
||||
|
} |
||||
|
if update.FictionalDate != nil { |
||||
|
updateBson["fictionalDate"] = *update.FictionalDate |
||||
|
story.FictionalDate = *update.FictionalDate |
||||
|
} |
||||
|
if update.UpdatedDate != nil { |
||||
|
updateBson["updatedDate"] = *update.UpdatedDate |
||||
|
story.UpdatedDate = *update.UpdatedDate |
||||
|
} |
||||
|
if update.Listed != nil { |
||||
|
updateBson["listed"] = *update.Listed |
||||
|
story.Listed = *update.Listed |
||||
|
} |
||||
|
|
||||
|
err := r.stories.UpdateId(story.ID, bson.M{"$set": updateBson}) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return &story, nil |
||||
|
} |
||||
|
|
||||
|
func (r *storyRepository) AddTag(ctx context.Context, story models.Story, tag models.Tag) error { |
||||
|
ci, err := r.stories.UpdateAll(bson.M{"_id": story.ID}, bson.M{"$addToSet": bson.M{"tags": tag}}) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
if ci.Updated == 0 { |
||||
|
return repositories.ErrTagExists |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (r *storyRepository) RemoveTag(ctx context.Context, story models.Story, tag models.Tag) error { |
||||
|
ci, err := r.stories.UpdateAll(bson.M{"_id": story.ID}, bson.M{"$pull": bson.M{"tags": tag}}) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
if ci.Updated == 0 { |
||||
|
return repositories.ErrTagDoesNotExist |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (r *storyRepository) Delete(ctx context.Context, story models.Story) error { |
||||
|
err := r.stories.RemoveId(story.ID) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
chapterIds := make([]string, 0, 8) |
||||
|
err = r.chapters.Find(bson.M{"storyId": story.ID}).Distinct("_id", &chapterIds) |
||||
|
if err != nil { |
||||
|
log.Println("Failed to find chapterIds:", err) |
||||
|
return nil |
||||
|
} |
||||
|
if len(chapterIds) > 0 { |
||||
|
c1, err := r.chapters.RemoveAll(bson.M{"storyId": story.ID}) |
||||
|
if err != nil { |
||||
|
log.Println("Failed to remove chapters:", err) |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
c2, err := r.chapters.RemoveAll(bson.M{"comments": bson.M{"$in": chapterIds}}) |
||||
|
if err != nil { |
||||
|
log.Println("Failed to remove comments:", err) |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
log.Printf("Removed story %s (%d chapters, %d comments)", story.ID, c1.Removed, c2.Removed) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
@ -0,0 +1,14 @@ |
|||||
|
package repositories |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"git.aiterp.net/rpdata/api/models" |
||||
|
) |
||||
|
|
||||
|
type ChapterRepository interface { |
||||
|
Find(ctx context.Context, id string) (*models.Chapter, error) |
||||
|
List(ctx context.Context, filter models.ChapterFilter) ([]*models.Chapter, error) |
||||
|
Insert(ctx context.Context, chapter models.Chapter) (*models.Chapter, error) |
||||
|
Update(ctx context.Context, chapter models.Chapter, update models.ChapterUpdate) (*models.Story, error) |
||||
|
Delete(ctx context.Context, chapter models.Chapter) error |
||||
|
} |
@ -0,0 +1,14 @@ |
|||||
|
package repositories |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"git.aiterp.net/rpdata/api/models" |
||||
|
) |
||||
|
|
||||
|
type CommentRepository interface { |
||||
|
Find(ctx context.Context, id string) (*models.Comment, error) |
||||
|
List(ctx context.Context, filter models.CommentFilter) ([]*models.Comment, error) |
||||
|
Insert(ctx context.Context, comment models.Comment) (*models.Comment, error) |
||||
|
Update(ctx context.Context, comment models.Comment, update models.CommentUpdate) (*models.Story, error) |
||||
|
Delete(ctx context.Context, comment models.Comment) error |
||||
|
} |
@ -0,0 +1,16 @@ |
|||||
|
package repositories |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"git.aiterp.net/rpdata/api/models" |
||||
|
) |
||||
|
|
||||
|
type StoryRepository interface { |
||||
|
Find(ctx context.Context, id string) (*models.Story, error) |
||||
|
List(ctx context.Context, filter models.StoryFilter) ([]*models.Story, error) |
||||
|
Insert(ctx context.Context, story models.Story) (*models.Story, error) |
||||
|
Update(ctx context.Context, story models.Story, update models.StoryUpdate) (*models.Story, error) |
||||
|
AddTag(ctx context.Context, story models.Story, tag models.Tag) error |
||||
|
RemoveTag(ctx context.Context, story models.Story, tag models.Tag) error |
||||
|
Delete(ctx context.Context, story models.Story) error |
||||
|
} |
@ -0,0 +1,152 @@ |
|||||
|
package services |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"errors" |
||||
|
"git.aiterp.net/rpdata/api/internal/auth" |
||||
|
"git.aiterp.net/rpdata/api/internal/generate" |
||||
|
"git.aiterp.net/rpdata/api/models" |
||||
|
"git.aiterp.net/rpdata/api/models/changekeys" |
||||
|
"git.aiterp.net/rpdata/api/repositories" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
// StoryService is a service governing all operations on stories and child objects.
|
||||
|
type StoryService struct { |
||||
|
stories repositories.StoryRepository |
||||
|
chapters repositories.ChapterRepository |
||||
|
comments repositories.CommentRepository |
||||
|
changeService *ChangeService |
||||
|
} |
||||
|
|
||||
|
func (s *StoryService) FindStory(ctx context.Context, id string) (*models.Story, error) { |
||||
|
return s.stories.Find(ctx, id) |
||||
|
} |
||||
|
|
||||
|
func (s *StoryService) ListStories(ctx context.Context, filter models.StoryFilter) ([]*models.Story, error) { |
||||
|
return s.stories.List(ctx, filter) |
||||
|
} |
||||
|
|
||||
|
func (s *StoryService) ListChapters(ctx context.Context, story models.Story) ([]*models.Chapter, error) { |
||||
|
return s.chapters.List(ctx, models.ChapterFilter{StoryID: &story.ID, Limit: 0}) |
||||
|
} |
||||
|
|
||||
|
func (s *StoryService) ListComments(ctx context.Context, chapter models.Chapter, limit int) ([]*models.Comment, error) { |
||||
|
return s.comments.List(ctx, models.CommentFilter{ChapterID: &chapter.ID, Limit: limit}) |
||||
|
} |
||||
|
|
||||
|
func (s *StoryService) CreateStory(ctx context.Context, name, author string, category models.StoryCategory, listed, open bool, tags []models.Tag, createdDate, fictionalDate time.Time) (*models.Story, error) { |
||||
|
story := &models.Story{ |
||||
|
Name: name, |
||||
|
Author: author, |
||||
|
Category: category, |
||||
|
Listed: listed, |
||||
|
Open: open, |
||||
|
Tags: tags, |
||||
|
CreatedDate: createdDate, |
||||
|
FictionalDate: fictionalDate, |
||||
|
UpdatedDate: createdDate, |
||||
|
} |
||||
|
|
||||
|
if err := auth.CheckPermission(ctx, "add", story); err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
story, err := s.stories.Insert(ctx, *story) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
s.changeService.Submit(ctx, "Story", "add", story.Listed, changekeys.Listed(story), story) |
||||
|
|
||||
|
return story, nil |
||||
|
} |
||||
|
|
||||
|
func (s *StoryService) CreateChapter(ctx context.Context, story models.Story, title, author, source string, createdDate time.Time, fictionalDate *time.Time, commentMode models.ChapterCommentMode) (*models.Chapter, error) { |
||||
|
chapter := &models.Chapter{ |
||||
|
ID: generate.ChapterID(), |
||||
|
StoryID: story.ID, |
||||
|
Title: title, |
||||
|
Author: author, |
||||
|
Source: source, |
||||
|
CreatedDate: createdDate, |
||||
|
EditedDate: createdDate, |
||||
|
CommentMode: commentMode, |
||||
|
CommentsLocked: false, |
||||
|
} |
||||
|
if fictionalDate != nil { |
||||
|
chapter.FictionalDate = *fictionalDate |
||||
|
} |
||||
|
|
||||
|
if story.Open { |
||||
|
if !auth.TokenFromContext(ctx).Permitted("member", "chapter.add") { |
||||
|
return nil, auth.ErrUnauthorized |
||||
|
} |
||||
|
} else { |
||||
|
if err := auth.CheckPermission(ctx, "add", chapter); err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
chapter, err := s.chapters.Insert(ctx, *chapter) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
if createdDate.After(story.UpdatedDate) { |
||||
|
_, _ = s.stories.Update(ctx, story, models.StoryUpdate{UpdatedDate: &createdDate}) |
||||
|
} |
||||
|
|
||||
|
s.changeService.Submit(ctx, "Chapter", "add", story.Listed, changekeys.Many(story, chapter), chapter) |
||||
|
|
||||
|
return chapter, nil |
||||
|
} |
||||
|
|
||||
|
// CreateComment adds a comment.
|
||||
|
func (s *StoryService) CreateComment(ctx context.Context, 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 |
||||
|
} |
||||
|
|
||||
|
if !chapter.CanComment() { |
||||
|
return nil, errors.New("comments are locked or disabled") |
||||
|
} |
||||
|
|
||||
|
if author == "" { |
||||
|
if token := auth.TokenFromContext(ctx); token != nil { |
||||
|
author = token.UserID |
||||
|
} else { |
||||
|
return nil, auth.ErrUnauthenticated |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
comment := &models.Comment{ |
||||
|
ID: generate.CommentID(), |
||||
|
ChapterID: chapter.ID, |
||||
|
Subject: subject, |
||||
|
Author: author, |
||||
|
CharacterName: characterName, |
||||
|
CharacterID: characterID, |
||||
|
FictionalDate: fictionalDate, |
||||
|
CreatedDate: createdDate, |
||||
|
EditedDate: createdDate, |
||||
|
Source: source, |
||||
|
} |
||||
|
if err := auth.CheckPermission(ctx, "add", comment); err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
comment, err := s.comments.Insert(ctx, *comment) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
if story, err := s.stories.Find(ctx, chapter.StoryID); err == nil { |
||||
|
s.changeService.Submit(ctx, "Comment", "add", story.Listed, changekeys.Many(story, chapter, comment), comment) |
||||
|
} else { |
||||
|
s.changeService.Submit(ctx, "Comment", "add", false, changekeys.Many(chapter, comment), comment) |
||||
|
} |
||||
|
|
||||
|
return comment, nil |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue