package handler import ( "context" "encoding/json" "fmt" "math" "time" "github.com/micro/micro/v3/service/errors" "github.com/micro/micro/v3/service/logger" "github.com/micro/micro/v3/service/store" "github.com/gosimple/slug" pb "github.com/micro/services/blog/posts/proto/posts" posts "github.com/micro/services/blog/posts/proto/posts" tags "github.com/micro/services/blog/tags/proto" ) const ( tagType = "post-tag" slugPrefix = "slug" idPrefix = "id" timeStampPrefix = "timestamp" ) type Post struct { ID string `json:"id"` Title string `json:"title"` Slug string `json:"slug"` Content string `json:"content"` CreateTimestamp int64 `json:"create_timestamp"` UpdateTimestamp int64 `json:"update_timestamp"` Tags []string `json:"tags"` } type Posts struct { Tags tags.TagsService } func (p *Posts) Save(ctx context.Context, req *posts.SaveRequest, rsp *posts.SaveResponse) error { if len(req.Id) == 0 { return errors.BadRequest("posts.save.input-check", "Id is missing") } // read by post records, err := store.Read(fmt.Sprintf("%v:%v", idPrefix, req.Id)) if err != nil && err != store.ErrNotFound { return errors.InternalServerError("posts.save.store-id-read", "Failed to read post by id: %v", err.Error()) } postSlug := slug.Make(req.Title) // If no existing record is found, create a new one if len(records) == 0 { post := &Post{ ID: req.Id, Title: req.Title, Content: req.Content, Tags: req.Tags, Slug: postSlug, CreateTimestamp: time.Now().Unix(), } err := p.savePost(ctx, nil, post) if err != nil { return errors.InternalServerError("posts.save.post-save", "Failed to save new post: %v", err.Error()) } return nil } record := records[0] oldPost := &Post{} err = json.Unmarshal(record.Value, oldPost) if err != nil { return errors.InternalServerError("posts.save.unmarshal", "Failed to unmarshal old post: %v", err.Error()) } post := &Post{ ID: req.Id, Title: oldPost.Title, Content: oldPost.Content, Slug: oldPost.Slug, Tags: oldPost.Tags, CreateTimestamp: oldPost.CreateTimestamp, UpdateTimestamp: time.Now().Unix(), } if len(req.Title) > 0 { post.Title = req.Title post.Slug = slug.Make(post.Title) } if len(req.Slug) > 0 { post.Slug = req.Slug } if len(req.Content) > 0 { post.Content = req.Content } if len(req.Tags) > 0 { // Handle the special case of deletion if len(req.Tags) == 0 && req.Tags[0] == "" { post.Tags = []string{} } else { post.Tags = req.Tags } } // Check if slug exists recordsBySlug, err := store.Read(fmt.Sprintf("%v:%v", slugPrefix, postSlug)) if err != nil && err != store.ErrNotFound { return errors.InternalServerError("posts.save.store-read", "Failed to read post by slug: %v", err.Error()) } if len(recordsBySlug) > 0 { otherSlugPost := &Post{} err = json.Unmarshal(recordsBySlug[0].Value, otherSlugPost) if oldPost.ID != otherSlugPost.ID { if err != nil { return errors.InternalServerError("posts.save.slug-unmarshal", "Error unmarshaling other post with same slug: %v", err.Error()) } } return errors.BadRequest("posts.save.slug-check", "An other post with this slug already exists") } return p.savePost(ctx, oldPost, post) } func (p *Posts) savePost(ctx context.Context, oldPost, post *Post) error { bytes, err := json.Marshal(post) if err != nil { return err } err = store.Write(&store.Record{ Key: fmt.Sprintf("%v:%v", idPrefix, post.ID), Value: bytes, }) if err != nil { return err } // Delete old slug index if the slug has changed if oldPost != nil && oldPost.Slug != post.Slug { err = store.Delete(fmt.Sprintf("%v:%v", slugPrefix, post.Slug)) if err != nil { return err } } err = store.Write(&store.Record{ Key: fmt.Sprintf("%v:%v", slugPrefix, post.Slug), Value: bytes, }) if err != nil { return err } err = store.Write(&store.Record{ Key: fmt.Sprintf("%v:%v", timeStampPrefix, math.MaxInt64-post.CreateTimestamp), Value: bytes, }) if err != nil { return err } if oldPost == nil { for _, tagName := range post.Tags { _, err := p.Tags.Add(ctx, &tags.AddRequest{ ResourceID: post.ID, Type: tagType, Title: tagName, }) if err != nil { return err } } return nil } return p.diffTags(ctx, post.ID, oldPost.Tags, post.Tags) } func (p *Posts) diffTags(ctx context.Context, parentID string, oldTagNames, newTagNames []string) error { oldTags := map[string]struct{}{} for _, v := range oldTagNames { oldTags[v] = struct{}{} } newTags := map[string]struct{}{} for _, v := range newTagNames { newTags[v] = struct{}{} } for i := range oldTags { _, stillThere := newTags[i] if !stillThere { _, err := p.Tags.Remove(ctx, &tags.RemoveRequest{ ResourceID: parentID, Type: tagType, Title: i, }) if err != nil { logger.Errorf("Error decreasing count for tag '%v' with type '%v' for parent '%v'", i, tagType, parentID) } } } for i := range newTags { _, newlyAdded := oldTags[i] if newlyAdded { _, err := p.Tags.Add(ctx, &tags.AddRequest{ ResourceID: parentID, Type: tagType, Title: i, }) if err != nil { logger.Errorf("Error increasing count for tag '%v' with type '%v' for parent '%v': %v", i, tagType, parentID, err) } } } return nil } func (p *Posts) Query(ctx context.Context, req *pb.QueryRequest, rsp *pb.QueryResponse) error { var records []*store.Record var err error if len(req.Slug) > 0 { key := fmt.Sprintf("%v:%v", slugPrefix, req.Slug) logger.Infof("Reading post by slug: %v", req.Slug) records, err = store.Read("", store.Prefix(key)) } else if len(req.Id) > 0 { key := fmt.Sprintf("%v:%v", idPrefix, req.Id) logger.Infof("Reading post by id: %v", req.Id) records, err = store.Read("", store.Prefix(key)) } else { key := fmt.Sprintf("%v:", timeStampPrefix) var limit uint limit = 20 if req.Limit > 0 { limit = uint(req.Limit) } logger.Infof("Listing posts, offset: %v, limit: %v", req.Offset, limit) records, err = store.Read("", store.Prefix(key), store.Offset(uint(req.Offset)), store.Limit(limit)) } if err != nil { return errors.BadRequest("posts.query.store-read", "Failed to read from store: %v", err.Error()) } rsp.Posts = make([]*pb.Post, len(records)) for i, record := range records { postRecord := &Post{} err := json.Unmarshal(record.Value, postRecord) if err != nil { return errors.InternalServerError("posts.save.unmarshal", "Failed to unmarshal old post: %v", err.Error()) } rsp.Posts[i] = &pb.Post{ Id: postRecord.ID, Title: postRecord.Title, Slug: postRecord.Slug, Content: postRecord.Content, Tags: postRecord.Tags, } } return nil } func (p *Posts) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.DeleteResponse) error { logger.Info("Received Post.Delete request") records, err := store.Read(fmt.Sprintf("%v:%v", idPrefix, req.Id)) if err != nil && err != store.ErrNotFound { return err } if len(records) == 0 { return fmt.Errorf("Post with ID %v not found", req.Id) } post := &Post{} err = json.Unmarshal(records[0].Value, post) if err != nil { return err } // Delete by ID err = store.Delete(fmt.Sprintf("%v:%v", idPrefix, post.ID)) if err != nil { return err } // Delete by slug err = store.Delete(fmt.Sprintf("%v:%v", slugPrefix, post.Slug)) if err != nil { return err } // Delete by timeStamp return store.Delete(fmt.Sprintf("%v:%v", timeStampPrefix, post.CreateTimestamp)) }