mirror of
https://github.com/kevin-DL/services.git
synced 2026-01-23 15:51:24 +00:00
Using model in blog/posts (#15)
This commit is contained in:
@@ -2,15 +2,14 @@ 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/micro/dev/model"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
pb "github.com/micro/services/blog/posts/proto/posts"
|
||||
posts "github.com/micro/services/blog/posts/proto/posts"
|
||||
@@ -18,24 +17,39 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
tagType = "post-tag"
|
||||
slugPrefix = "slug"
|
||||
idPrefix = "id"
|
||||
timeStampPrefix = "timestamp"
|
||||
tagType = "post-tag"
|
||||
)
|
||||
|
||||
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"`
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Slug string `json:"slug"`
|
||||
Content string `json:"content"`
|
||||
Created int64 `json:"created"`
|
||||
Updated int64 `json:"updated"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
type Posts struct {
|
||||
Tags tags.TagsService
|
||||
db model.DB
|
||||
}
|
||||
|
||||
func NewPosts(tagsService tags.TagsService) *Posts {
|
||||
createdIndex := model.ByEquality("created")
|
||||
createdIndex.Order.Type = model.OrderTypeDesc
|
||||
|
||||
return &Posts{
|
||||
Tags: tagsService,
|
||||
db: model.NewDB(
|
||||
store.DefaultStore,
|
||||
"posts",
|
||||
model.Indexes(model.ByEquality("slug"), createdIndex),
|
||||
&model.DBOptions{
|
||||
Debug: false,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Posts) Save(ctx context.Context, req *posts.SaveRequest, rsp *posts.SaveResponse) error {
|
||||
@@ -44,20 +58,23 @@ func (p *Posts) Save(ctx context.Context, req *posts.SaveRequest, rsp *posts.Sav
|
||||
}
|
||||
|
||||
// read by post
|
||||
records, err := store.Read(fmt.Sprintf("%v:%v", idPrefix, req.Id))
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
posts := []Post{}
|
||||
q := model.Equals("id", req.Id)
|
||||
q.Order.Type = model.OrderTypeUnordered
|
||||
err := p.db.List(q, &posts)
|
||||
if err != nil {
|
||||
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 {
|
||||
if len(posts) == 0 {
|
||||
post := &Post{
|
||||
ID: req.Id,
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
Tags: req.Tags,
|
||||
Slug: postSlug,
|
||||
CreateTimestamp: time.Now().Unix(),
|
||||
ID: req.Id,
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
Tags: req.Tags,
|
||||
Slug: postSlug,
|
||||
Created: time.Now().Unix(),
|
||||
}
|
||||
err := p.savePost(ctx, nil, post)
|
||||
if err != nil {
|
||||
@@ -65,21 +82,16 @@ func (p *Posts) Save(ctx context.Context, req *posts.SaveRequest, rsp *posts.Sav
|
||||
}
|
||||
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())
|
||||
}
|
||||
oldPost := &posts[0]
|
||||
|
||||
post := &Post{
|
||||
ID: req.Id,
|
||||
Title: oldPost.Title,
|
||||
Content: oldPost.Content,
|
||||
Slug: oldPost.Slug,
|
||||
Tags: oldPost.Tags,
|
||||
CreateTimestamp: oldPost.CreateTimestamp,
|
||||
UpdateTimestamp: time.Now().Unix(),
|
||||
ID: req.Id,
|
||||
Title: oldPost.Title,
|
||||
Content: oldPost.Content,
|
||||
Slug: oldPost.Slug,
|
||||
Tags: oldPost.Tags,
|
||||
Created: oldPost.Created,
|
||||
Updated: time.Now().Unix(),
|
||||
}
|
||||
if len(req.Title) > 0 {
|
||||
post.Title = req.Title
|
||||
@@ -100,56 +112,23 @@ func (p *Posts) Save(ctx context.Context, req *posts.SaveRequest, rsp *posts.Sav
|
||||
}
|
||||
}
|
||||
|
||||
// Check if slug exists
|
||||
recordsBySlug, err := store.Read(fmt.Sprintf("%v:%v", slugPrefix, postSlug))
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
postsWithThisSlug := []Post{}
|
||||
err = p.db.List(model.Equals("slug", postSlug), &postsWithThisSlug)
|
||||
if err != nil {
|
||||
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())
|
||||
}
|
||||
|
||||
if len(postsWithThisSlug) > 0 {
|
||||
if oldPost.ID != postsWithThisSlug[0].ID {
|
||||
return errors.BadRequest("posts.save.slug-check", "An other post with this slug already exists")
|
||||
}
|
||||
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,
|
||||
})
|
||||
err := p.db.Save(post)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -208,45 +187,40 @@ func (p *Posts) diffTags(ctx context.Context, parentID string, oldTagNames, newT
|
||||
}
|
||||
|
||||
func (p *Posts) Query(ctx context.Context, req *pb.QueryRequest, rsp *pb.QueryResponse) error {
|
||||
var records []*store.Record
|
||||
var err error
|
||||
var q model.Query
|
||||
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))
|
||||
q = model.Equals("slug", req.Slug)
|
||||
} 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))
|
||||
q = model.Equals("id", req.Id)
|
||||
q.Order.Type = model.OrderTypeUnordered
|
||||
} else {
|
||||
key := fmt.Sprintf("%v:", timeStampPrefix)
|
||||
q = model.Equals("created", nil)
|
||||
q.Order.Type = model.OrderTypeDesc
|
||||
var limit uint
|
||||
limit = 20
|
||||
if req.Limit > 0 {
|
||||
limit = uint(req.Limit)
|
||||
}
|
||||
q.Limit = int64(limit)
|
||||
q.Offset = req.Offset
|
||||
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))
|
||||
}
|
||||
|
||||
posts := []Post{}
|
||||
err := p.db.List(q, &posts)
|
||||
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 = make([]*pb.Post, len(posts))
|
||||
for i, post := range posts {
|
||||
rsp.Posts[i] = &pb.Post{
|
||||
Id: postRecord.ID,
|
||||
Title: postRecord.Title,
|
||||
Slug: postRecord.Slug,
|
||||
Content: postRecord.Content,
|
||||
Tags: postRecord.Tags,
|
||||
Id: post.ID,
|
||||
Title: post.Title,
|
||||
Slug: post.Slug,
|
||||
Content: post.Content,
|
||||
Tags: post.Tags,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -254,29 +228,5 @@ func (p *Posts) Query(ctx context.Context, req *pb.QueryRequest, rsp *pb.QueryRe
|
||||
|
||||
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))
|
||||
return p.db.Delete(model.Equals("id", req.Id))
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ func main() {
|
||||
)
|
||||
|
||||
// Register Handler
|
||||
srv.Handle(&handler.Posts{
|
||||
Tags: tags.NewTagsService("tags", srv.Client()),
|
||||
})
|
||||
srv.Handle(handler.NewPosts(
|
||||
tags.NewTagsService("tags", srv.Client()),
|
||||
))
|
||||
|
||||
// Run service
|
||||
if err := srv.Run(); err != nil {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/micro/dev/model"
|
||||
"github.com/micro/micro/v3/service/errors"
|
||||
"github.com/micro/micro/v3/service/logger"
|
||||
"github.com/micro/micro/v3/service/store"
|
||||
@@ -14,9 +15,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
slugPrefix = "bySlug"
|
||||
resourcePrefix = "byResource"
|
||||
typePrefix = "byType"
|
||||
tagCountPrefix = "tagCount"
|
||||
childrenByTag = "childrenByTag"
|
||||
)
|
||||
@@ -28,37 +27,50 @@ type Tag struct {
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
type Tags struct{}
|
||||
type Tags struct {
|
||||
db model.DB
|
||||
}
|
||||
|
||||
func NewTags() *Tags {
|
||||
slugIndex := model.ByEquality("slug")
|
||||
slugIndex.Order.Type = model.OrderTypeUnordered
|
||||
return &Tags{
|
||||
db: model.NewDB(
|
||||
store.DefaultStore,
|
||||
"tags",
|
||||
model.Indexes(model.ByEquality("type")),
|
||||
&model.DBOptions{
|
||||
IdIndex: slugIndex,
|
||||
Debug: false,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tags) Add(ctx context.Context, req *pb.AddRequest, rsp *pb.AddResponse) error {
|
||||
if len(req.ResourceID) == 0 || len(req.Type) == 0 {
|
||||
return errors.BadRequest("tags.increasecount.input-check", "resource id and type is required")
|
||||
}
|
||||
|
||||
tags := []Tag{}
|
||||
tagSlug := slug.Make(req.GetTitle())
|
||||
key := fmt.Sprintf("%v:%v", slugPrefix, tagSlug)
|
||||
|
||||
// read by resource ID + slug, the record is identical in boths places anyway
|
||||
records, err := store.Read(key)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
q := model.Equals("slug", tagSlug)
|
||||
q.Order.Type = model.OrderTypeUnordered
|
||||
err := t.db.List(q, &tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var tag *Tag
|
||||
// If no existing record is found, create a new one
|
||||
if len(records) == 0 {
|
||||
if len(tags) == 0 {
|
||||
tag = &Tag{
|
||||
Title: req.GetTitle(),
|
||||
Type: req.Type,
|
||||
Slug: tagSlug,
|
||||
}
|
||||
} else {
|
||||
record := records[0]
|
||||
tag = &Tag{}
|
||||
err = json.Unmarshal(record.Value, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tag = &tags[0]
|
||||
}
|
||||
|
||||
// increase tag count
|
||||
@@ -96,28 +108,8 @@ func (t *Tags) Add(ctx context.Context, req *pb.AddRequest, rsp *pb.AddResponse)
|
||||
}
|
||||
|
||||
func (t *Tags) saveTag(tag *Tag) error {
|
||||
tagSlug := slug.Make(tag.Title)
|
||||
|
||||
key := fmt.Sprintf("%v:%v", slugPrefix, tagSlug)
|
||||
typeKey := fmt.Sprintf("%v:%v:%v", typePrefix, tag.Type, tagSlug)
|
||||
|
||||
bytes, err := json.Marshal(tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write resourceId:slug to enable prefix listing based on type
|
||||
err = store.Write(&store.Record{
|
||||
Key: key,
|
||||
Value: bytes,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return store.Write(&store.Record{
|
||||
Key: typeKey,
|
||||
Value: bytes,
|
||||
})
|
||||
tag.Slug = slug.Make(tag.Title)
|
||||
return t.db.Save(tag)
|
||||
}
|
||||
|
||||
func (t *Tags) Remove(ctx context.Context, req *pb.RemoveRequest, rsp *pb.RemoveResponse) error {
|
||||
@@ -163,15 +155,37 @@ func (t *Tags) Remove(ctx context.Context, req *pb.RemoveRequest, rsp *pb.Remove
|
||||
|
||||
func (t *Tags) List(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error {
|
||||
logger.Info("Received Tags.List request")
|
||||
|
||||
// unfortunately there is a mixing of manual indexes
|
||||
// and model here because model does not yet support
|
||||
// many to many relations
|
||||
key := ""
|
||||
var q model.Query
|
||||
if len(req.ResourceID) > 0 {
|
||||
key = fmt.Sprintf("%v:%v", resourcePrefix, req.ResourceID)
|
||||
} else if len(req.Type) > 0 {
|
||||
key = fmt.Sprintf("%v:%v", typePrefix, req.Type)
|
||||
q = model.Equals("type", req.Type)
|
||||
} else {
|
||||
return errors.BadRequest("tags.list.input-check", "resource id or type is required")
|
||||
}
|
||||
|
||||
if q.Type != "" {
|
||||
tags := []Tag{}
|
||||
err := t.db.List(q, &tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rsp.Tags = make([]*pb.Tag, len(tags))
|
||||
for i, tag := range tags {
|
||||
rsp.Tags[i] = &pb.Tag{
|
||||
Title: tag.Title,
|
||||
Type: tag.Type,
|
||||
Slug: tag.Slug,
|
||||
Count: tag.Count,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
records, err := store.Read("", store.Prefix(key))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -201,23 +215,16 @@ func (t *Tags) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.Update
|
||||
}
|
||||
|
||||
tagSlug := slug.Make(req.GetTitle())
|
||||
resourceID := fmt.Sprintf("%v:%v", slugPrefix, tagSlug)
|
||||
|
||||
// read by resource ID + slug, the record is identical in boths places anyway
|
||||
records, err := store.Read(resourceID)
|
||||
tags := []Tag{}
|
||||
q := model.Equals("slug", tagSlug)
|
||||
q.Order.Type = model.OrderTypeUnordered
|
||||
err := t.db.List(q, &tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(records) == 0 {
|
||||
return fmt.Errorf("Tag with slug '%v' not found, nothing to update", tagSlug)
|
||||
if len(tags) == 0 {
|
||||
return errors.BadRequest("tags.update.input-check", "Tag not found")
|
||||
}
|
||||
record := records[0]
|
||||
tag := &Tag{}
|
||||
err = json.Unmarshal(record.Value, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tag.Title = req.Title
|
||||
return t.saveTag(tag)
|
||||
tags[0].Title = req.Title
|
||||
return t.saveTag(&tags[0])
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func main() {
|
||||
)
|
||||
|
||||
// Register Handler
|
||||
srv.Handle(new(handler.Tags))
|
||||
srv.Handle(handler.NewTags())
|
||||
|
||||
// Run service
|
||||
if err := srv.Run(); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user