Skip to content

Commit

Permalink
Merge
Browse files Browse the repository at this point in the history
  • Loading branch information
ben-toogood committed Oct 15, 2020
2 parents 930ccae + 5c2efed commit 5a7d104
Show file tree
Hide file tree
Showing 58 changed files with 868 additions and 1,171 deletions.
56 changes: 56 additions & 0 deletions .github/workflows/integration-blog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Blog integration tests
on: [push]

jobs:

test:
name: Blog integration tests
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v2
with:
go-version: 1.13
id: go

- name: Install Protoc
uses: arduino/setup-protoc@master

- name: Check out this code
uses: actions/checkout@v2
with:
path: services

- name: Check out micro code
uses: actions/checkout@v2
with:
repository: 'micro/micro'
path: 'micro'
ref: '20cabee1960e6abe8b59d8f178ddf66ad5da1097'

- name: Enable caching
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Install micro
working-directory: micro
run: |
go run . init --profile=ci --output=profile.go
go mod edit -replace github.com/micro/micro/plugin/etcd/v3=./plugin/etcd
go mod edit -replace github.com/micro/micro/profile/ci/v3=./profile/ci
go mod edit -replace google.golang.org/grpc=google.golang.org/[email protected]
go install
- name: Build container
run: |
bash services/test/image/test-docker.sh
- name: Test Blog services
working-directory: services/test/integration
run: |
go clean -testcache && GOMAXPROCS=4 go test -timeout 15m --tags=blog -v ./...
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Services provides a home for real world examples for using Micro v3.

- [blog](blog) - A blog app composed as micro services
- [helloworld](helloworld) - A simple helloworld service
- [test](test) - A set of sample test services for Micro

## Usage

Expand Down
44 changes: 44 additions & 0 deletions blog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,47 @@ This is a full end to end example of writing a multi-service blog application
## Usage

Check out the [blog tutorial](https://m3o.dev/tutorials/building-a-blog) on the developer docs.

## How it works

### Present

The blog services are designed so a user can deploy them to their own micro namespace, write content with their Micro account with commands like

```sh
micro posts save --id=7 --tags=News,Finance --title="Breaking News" --content="The stock market has just crashed"
```

and display content on their frontend by consuming the API:

```sh
curl -H "Authorization: Bearer $MICRO_API_TOKEN" "Micro-Namespace: $NAMESPACE" https://api.m3o.com/tags/list


{
"tags": [
{
"type": "post-tag",
"slug": "news",
"title": "News",
"count": "3"
}
]
]
```
There are no comments provided yet, just posts and tags.
Access is governed by auth rules, ie. Posts List, Tags List is open, Posts Save requires a Micro login.
### Future possibilities
#### Enable non Micro users to write posts, comments
If we provide a user/login service (markedly different from auth, it can be a simple session based auth) to enable non Micro users to register, the following can be done:
- A user (let's call the user Alice from this point) launches posts, tags, login service in their namespace.
- Alice opens up said endpoints
- People (let's call them Yoga Pants Co and Drone Inc) hosting JS and HTML on Netlify or Github Pages could create accounts in the services hosted by Alice. In this way, Alice, by having a Micro account becomes a headless CMS provider. Multiple blogs can be created on top of Alice's service instances.
Questions:
- How will Yoga Pants Co or Drone Inc pay Alice or M3O for the costs of their backend hosting?
2 changes: 1 addition & 1 deletion blog/posts/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ MODIFY=Mgithub.com/micro/go-micro/api/proto/api.proto=github.com/micro/go-micro/
.PHONY: proto
proto:

protoc --proto_path=. --micro_out=${MODIFY}:. --go_out=${MODIFY}:. proto/post/post.proto
protoc --proto_path=. --micro_out=${MODIFY}:. --go_out=${MODIFY}:. proto/posts/posts.proto


.PHONY: build
Expand Down
61 changes: 30 additions & 31 deletions blog/posts/handler/posts.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"time"

"github.com/micro/go-micro/v3/errors"
gostore "github.com/micro/go-micro/v3/store"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/micro/v3/service/store"

Expand All @@ -32,31 +31,31 @@ type Post struct {
Content string `json:"content"`
CreateTimestamp int64 `json:"create_timestamp"`
UpdateTimestamp int64 `json:"update_timestamp"`
TagNames []string `json:"tagNames"`
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.Post.Id) == 0 || len(req.Post.Title) == 0 || len(req.Post.Content) == 0 {
if len(req.Id) == 0 || len(req.Title) == 0 || len(req.Content) == 0 {
return errors.BadRequest("posts.save.input-check", "Id, title or content is missing")
}

// read by post
records, err := store.Read(fmt.Sprintf("%v:%v", idPrefix, req.Post.Id))
if err != nil && err != gostore.ErrNotFound {
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.Post.Title)
postSlug := slug.Make(req.Title)
// If no existing record is found, create a new one
if len(records) == 0 {
post := &Post{
ID: req.Post.Id,
Title: req.Post.Title,
Content: req.Post.Content,
TagNames: req.Post.TagNames,
ID: req.Id,
Title: req.Title,
Content: req.Content,
Tags: req.Tags,
Slug: postSlug,
CreateTimestamp: time.Now().Unix(),
}
Expand All @@ -73,18 +72,18 @@ func (p *Posts) Save(ctx context.Context, req *posts.SaveRequest, rsp *posts.Sav
return errors.InternalServerError("posts.save.unmarshal", "Failed to unmarshal old post: %v", err.Error())
}
post := &Post{
ID: req.Post.Id,
Title: req.Post.Title,
Content: req.Post.Content,
ID: req.Id,
Title: req.Title,
Content: req.Content,
Slug: postSlug,
TagNames: req.Post.TagNames,
Tags: req.Tags,
CreateTimestamp: oldPost.CreateTimestamp,
UpdateTimestamp: time.Now().Unix(),
}

// Check if slug exists
recordsBySlug, err := store.Read(fmt.Sprintf("%v:%v", slugPrefix, postSlug))
if err != nil && err != gostore.ErrNotFound {
if err != nil && err != store.ErrNotFound {
return errors.InternalServerError("posts.save.store-read", "Failed to read post by slug: %v", err.Error())
}
otherSlugPost := &Post{}
Expand All @@ -105,7 +104,7 @@ func (p *Posts) savePost(ctx context.Context, oldPost, post *Post) error {
return err
}

err = store.Write(&gostore.Record{
err = store.Write(&store.Record{
Key: fmt.Sprintf("%v:%v", idPrefix, post.ID),
Value: bytes,
})
Expand All @@ -119,23 +118,23 @@ func (p *Posts) savePost(ctx context.Context, oldPost, post *Post) error {
return err
}
}
err = store.Write(&gostore.Record{
err = store.Write(&store.Record{
Key: fmt.Sprintf("%v:%v", slugPrefix, post.Slug),
Value: bytes,
})
if err != nil {
return err
}
err = store.Write(&gostore.Record{
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.TagNames {
_, err := p.Tags.IncreaseCount(ctx, &tags.IncreaseCountRequest{
for _, tagName := range post.Tags {
_, err := p.Tags.Add(ctx, &tags.AddRequest{
ParentID: post.ID,
Type: tagType,
Title: tagName,
Expand All @@ -146,7 +145,7 @@ func (p *Posts) savePost(ctx context.Context, oldPost, post *Post) error {
}
return nil
}
return p.diffTags(ctx, post.ID, oldPost.TagNames, post.TagNames)
return p.diffTags(ctx, post.ID, oldPost.Tags, post.Tags)
}

func (p *Posts) diffTags(ctx context.Context, parentID string, oldTagNames, newTagNames []string) error {
Expand All @@ -161,7 +160,7 @@ func (p *Posts) diffTags(ctx context.Context, parentID string, oldTagNames, newT
for i := range oldTags {
_, stillThere := newTags[i]
if !stillThere {
_, err := p.Tags.DecreaseCount(ctx, &tags.DecreaseCountRequest{
_, err := p.Tags.Remove(ctx, &tags.RemoveRequest{
ParentID: parentID,
Type: tagType,
Title: i,
Expand All @@ -174,21 +173,21 @@ func (p *Posts) diffTags(ctx context.Context, parentID string, oldTagNames, newT
for i := range newTags {
_, newlyAdded := oldTags[i]
if newlyAdded {
_, err := p.Tags.IncreaseCount(ctx, &tags.IncreaseCountRequest{
_, err := p.Tags.Add(ctx, &tags.AddRequest{
ParentID: parentID,
Type: tagType,
Title: i,
})
if err != nil {
logger.Errorf("Error increasing count for tag '%v' with type '%v' for parent '%v'", i, tagType, parentID)
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 []*gostore.Record
var records []*store.Record
var err error
if len(req.Slug) > 0 {
key := fmt.Sprintf("%v:%v", slugPrefix, req.Slug)
Expand Down Expand Up @@ -218,11 +217,11 @@ func (p *Posts) Query(ctx context.Context, req *pb.QueryRequest, rsp *pb.QueryRe
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,
TagNames: postRecord.TagNames,
Id: postRecord.ID,
Title: postRecord.Title,
Slug: postRecord.Slug,
Content: postRecord.Content,
Tags: postRecord.Tags,
}
}
return nil
Expand All @@ -231,7 +230,7 @@ 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 != gostore.ErrNotFound {
if err != nil && err != store.ErrNotFound {
return err
}
if len(records) == 0 {
Expand Down
Loading

0 comments on commit 5a7d104

Please sign in to comment.