remove the services we won't use

This commit is contained in:
Asim Aslam
2021-05-12 19:26:23 +01:00
parent d3b667c0bf
commit c774879044
147 changed files with 0 additions and 16582 deletions

2
chats/.gitignore vendored
View File

@@ -1,2 +0,0 @@
chats

View File

@@ -1,3 +0,0 @@
FROM alpine
ADD chats /chats
ENTRYPOINT [ "/chats" ]

View File

@@ -1,26 +0,0 @@
GOPATH:=$(shell go env GOPATH)
.PHONY: init
init:
go get -u github.com/golang/protobuf/proto
go get -u github.com/golang/protobuf/protoc-gen-go
go get github.com/micro/micro/v3/cmd/protoc-gen-micro
.PHONY: proto
proto:
protoc --openapi_out=. --proto_path=. --micro_out=. --go_out=. proto/chats.proto
docs:
protoc --openapi_out=. --proto_path=. --micro_out=. --go_out=. proto/chats.proto
@redoc-cli bundle api-chats.json
.PHONY: build
build:
go build -o chats *.go
.PHONY: test
test:
go test -v ./... -cover
.PHONY: docker
docker:
docker build . -t chats:latest

View File

@@ -1,5 +0,0 @@
Chats is a service for direct messaging
# Chats Service
The chats service enables direct messaging between one or more parties.

View File

@@ -1,3 +0,0 @@
package main
//go:generate make proto

View File

@@ -1,126 +0,0 @@
package handler
import (
"context"
"fmt"
"sort"
"strings"
"time"
pb "github.com/micro/services/chats/proto"
"github.com/micro/services/pkg/tenant"
"github.com/micro/micro/v3/service/errors"
)
var (
ErrMissingID = errors.BadRequest("MISSING_ID", "Missing ID")
ErrMissingAuthorID = errors.BadRequest("MISSING_AUTHOR_ID", "Missing Author ID")
ErrMissingText = errors.BadRequest("MISSING_TEXT", "Missing text")
ErrMissingChatID = errors.BadRequest("MISSING_CHAT_ID", "Missing Chat ID")
ErrMissingUserIDs = errors.BadRequest("MISSING_USER_IDs", "Two or more user IDs are required")
ErrNotFound = errors.NotFound("NOT_FOUND", "Chat not found")
)
type Chats struct {
Time func() time.Time
}
type Chat struct {
ID string
UserIDs []string
CreatedAt time.Time
}
type Message struct {
ID string
AuthorID string
ChatID string
Text string
SentAt time.Time
}
func ParseTime(v string) time.Time {
t, err := time.Parse(time.RFC3339Nano, v)
if err == nil {
return t
}
t, err = time.Parse(time.RFC3339, v)
if err == nil {
return t
}
return time.Time{}
}
func (m *Message) Serialize() *pb.Message {
return &pb.Message{
Id: m.ID,
AuthorId: m.AuthorID,
ChatId: m.ChatID,
Text: m.Text,
SentAt: m.SentAt.Format(time.RFC3339Nano),
}
}
func (c *Chat) Index(ctx context.Context) string {
sort.Strings(c.UserIDs)
users := strings.Join(c.UserIDs, "-")
key := fmt.Sprintf("chatByUserIDs:%s", users)
t, ok := tenant.FromContext(ctx)
if !ok {
return key
}
return fmt.Sprintf("%s/%s", t, key)
}
func (c *Chat) Key(ctx context.Context) string {
key := fmt.Sprintf("chat:%s", c.ID)
t, ok := tenant.FromContext(ctx)
if !ok {
return key
}
return fmt.Sprintf("%s/%s", t, key)
}
func (m *Message) Key(ctx context.Context) string {
key := fmt.Sprintf("message:%s:%s", m.ChatID, m.ID)
t, ok := tenant.FromContext(ctx)
if !ok {
return key
}
return fmt.Sprintf("%s/%s", t, key)
}
func (m *Message) Index(ctx context.Context) string {
key := fmt.Sprintf("messagesByChatID:%s", m.ChatID)
if !m.SentAt.IsZero() {
key = fmt.Sprintf("%s:%d", key, m.SentAt.UnixNano())
if len(m.ID) > 0 {
key = fmt.Sprintf("%s:%s", key, m.ID)
}
}
t, ok := tenant.FromContext(ctx)
if !ok {
return key
}
return fmt.Sprintf("%s/%s", t, key)
}
func (c *Chat) Serialize() *pb.Chat {
return &pb.Chat{
Id: c.ID,
UserIds: c.UserIDs,
CreatedAt: c.CreatedAt.Format(time.RFC3339Nano),
}
}

View File

@@ -1,74 +0,0 @@
package handler_test
import (
"context"
"testing"
"time"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/store"
"github.com/micro/micro/v3/service/store/memory"
"github.com/micro/services/chats/handler"
pb "github.com/micro/services/chats/proto"
"github.com/stretchr/testify/assert"
)
func testHandler(t *testing.T) *handler.Chats {
store.DefaultStore = memory.NewStore()
return &handler.Chats{Time: func() time.Time { return time.Unix(1611327673, 0) }}
}
func assertChatsMatch(t *testing.T, exp, act *pb.Chat) {
if act == nil {
t.Errorf("Chat not returned")
return
}
// adapt this check so we can reuse the func in testing create, where we don't know the exact id
// which will be generated
if len(exp.Id) > 0 {
assert.Equal(t, exp.Id, act.Id)
} else {
assert.NotEmpty(t, act.Id)
}
assert.Equal(t, exp.UserIds, act.UserIds)
if len(act.CreatedAt) == 0 {
t.Errorf("CreatedAt not set")
return
}
assert.True(t, exp.CreatedAt == act.CreatedAt)
}
func assertMessagesMatch(t *testing.T, exp, act *pb.Message) {
if act == nil {
t.Errorf("Message not returned")
return
}
// adapt these checks so we can reuse the func in testing create, where we don't know the exact id /
// idempotent_id which will be generated
if len(exp.Id) > 0 {
assert.Equal(t, exp.Id, act.Id)
} else {
assert.NotEmpty(t, act.Id)
}
assert.Equal(t, exp.Text, act.Text)
assert.Equal(t, exp.AuthorId, act.AuthorId)
assert.Equal(t, exp.ChatId, act.ChatId)
if len(act.SentAt) == 0 {
t.Errorf("SentAt not set")
return
}
assert.True(t, exp.SentAt == act.SentAt)
}
func microAccountCtx() context.Context {
return auth.ContextWithAccount(context.TODO(), &auth.Account{
Issuer: "micro",
})
}

View File

@@ -1,80 +0,0 @@
package handler
import (
"context"
"sort"
"time"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/micro/v3/service/store"
pb "github.com/micro/services/chats/proto"
)
// Create a chat between two or more users, if a chat already exists for these users, the existing
// chat will be returned
func (c *Chats) CreateChat(ctx context.Context, req *pb.CreateChatRequest, rsp *pb.CreateChatResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.UserIds) < 2 {
return ErrMissingUserIDs
}
// sort the user ids
sort.Strings(req.UserIds)
id := uuid.New().String()
if len(req.Id) > 0 {
id = req.Id
}
// construct the chat
chat := &Chat{
ID: id,
CreatedAt: time.Now(),
UserIDs: req.UserIds,
}
// read the chat by the unique composition of ids
recs, err := store.Read(chat.Key(ctx), store.ReadLimit(1))
if err == nil && len(recs) == 1 {
// found an existing record
recs[0].Decode(&chat)
rsp.Chat = chat.Serialize()
return nil
}
// if not found check it exists by user index key
if err == store.ErrNotFound {
recs, err = store.Read(chat.Index(ctx), store.ReadLimit(1))
if err == nil && len(recs) > 0 {
recs[0].Decode(&chat)
rsp.Chat = chat.Serialize()
return nil
}
}
// ok otherwise we're creating an entirely new record
newRec := store.NewRecord(chat.Key(ctx), chat)
if err := store.Write(newRec); err != nil {
logger.Errorf("Error creating chat: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to database")
}
// write the user composite key
newRec = store.NewRecord(chat.Index(ctx), chat)
if err := store.Write(newRec); err != nil {
logger.Errorf("Error creating chat: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to database")
}
// return the record
rsp.Chat = chat.Serialize()
return nil
}

View File

@@ -1,62 +0,0 @@
package handler_test
import (
"testing"
"github.com/google/uuid"
"github.com/micro/services/chats/handler"
pb "github.com/micro/services/chats/proto"
"github.com/stretchr/testify/assert"
)
func TestCreateChat(t *testing.T) {
userIDs := []string{uuid.New().String(), uuid.New().String()}
tt := []struct {
Name string
UserIDs []string
Error error
}{
{
Name: "NoUserIDs",
Error: handler.ErrMissingUserIDs,
},
{
Name: "OneUserID",
UserIDs: userIDs[1:],
Error: handler.ErrMissingUserIDs,
},
{
Name: "Valid",
UserIDs: userIDs,
},
{
Name: "Repeat",
UserIDs: userIDs,
},
}
var chat *pb.Chat
h := testHandler(t)
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
var rsp pb.CreateChatResponse
err := h.CreateChat(microAccountCtx(), &pb.CreateChatRequest{
UserIds: tc.UserIDs,
}, &rsp)
assert.Equal(t, tc.Error, err)
if tc.Error != nil {
return
}
assert.NotNil(t, rsp.Chat)
if chat == nil {
chat = rsp.Chat
} else {
assertChatsMatch(t, chat, rsp.Chat)
}
})
}
}

View File

@@ -1,87 +0,0 @@
package handler
import (
"context"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/micro/v3/service/store"
pb "github.com/micro/services/chats/proto"
)
// Create a message within a chat
func (c *Chats) SendMessage(ctx context.Context, req *pb.SendMessageRequest, rsp *pb.SendMessageResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.AuthorId) == 0 {
return ErrMissingAuthorID
}
if len(req.ChatId) == 0 {
return ErrMissingChatID
}
if len(req.Text) == 0 {
return ErrMissingText
}
chat := &Chat{
ID: req.ChatId,
}
recs, err := store.Read(chat.Key(ctx), store.ReadLimit(1))
if err == store.ErrNotFound {
return ErrNotFound
} else if err != nil {
logger.Errorf("Error reading chat: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to database")
}
// create the message
msg := &Message{
ID: req.Id,
Text: req.Text,
AuthorID: req.AuthorId,
ChatID: req.ChatId,
SentAt: c.Time(),
}
if len(msg.ID) == 0 {
msg.ID = uuid.New().String()
}
// check if the message already exists
recs, err = store.Read(msg.Key(ctx), store.ReadLimit(1))
if err == nil && len(recs) == 1 {
// return the existing message
msg = &Message{}
recs[0].Decode(&msg)
rsp.Message = msg.Serialize()
return nil
}
// if there's an error then return
if err != nil && err != store.ErrNotFound {
logger.Errorf("Error creating message: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to database")
}
// otherwise write the record
if err := store.Write(store.NewRecord(msg.Key(ctx), msg)); err != nil {
logger.Errorf("Error creating message: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to database")
}
// write the time based index
if err := store.Write(store.NewRecord(msg.Index(ctx), msg)); err == nil {
rsp.Message = msg.Serialize()
return nil
} else if err != nil {
logger.Errorf("Error creating message: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to database")
}
return nil
}

View File

@@ -1,108 +0,0 @@
package handler_test
import (
"testing"
"time"
"github.com/micro/services/chats/handler"
pb "github.com/micro/services/chats/proto"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestSendMessage(t *testing.T) {
h := testHandler(t)
// seed some data
var cRsp pb.CreateChatResponse
err := h.CreateChat(microAccountCtx(), &pb.CreateChatRequest{
UserIds: []string{uuid.New().String(), uuid.New().String()},
}, &cRsp)
if err != nil {
t.Fatalf("Error creating chat: %v", err)
return
}
iid := uuid.New().String()
tt := []struct {
Name string
AuthorID string
ChatID string
Text string
Error error
ID string
}{
{
Name: "MissingChatID",
Text: "HelloWorld",
AuthorID: uuid.New().String(),
Error: handler.ErrMissingChatID,
},
{
Name: "MissingAuthorID",
ChatID: uuid.New().String(),
Text: "HelloWorld",
Error: handler.ErrMissingAuthorID,
},
{
Name: "MissingText",
ChatID: uuid.New().String(),
AuthorID: uuid.New().String(),
Error: handler.ErrMissingText,
},
{
Name: "ChatNotFound",
ChatID: uuid.New().String(),
AuthorID: uuid.New().String(),
Text: "HelloWorld",
Error: handler.ErrNotFound,
},
{
Name: "WithoutID",
ChatID: cRsp.Chat.Id,
AuthorID: uuid.New().String(),
Text: "HelloWorld",
},
{
Name: "WithID",
ChatID: cRsp.Chat.Id,
AuthorID: "johndoe",
Text: "HelloWorld",
ID: iid,
},
{
Name: "RepeatID",
ChatID: cRsp.Chat.Id,
AuthorID: "johndoe",
Text: "HelloWorld",
ID: iid,
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
var rsp pb.SendMessageResponse
err := h.SendMessage(microAccountCtx(), &pb.SendMessageRequest{
AuthorId: tc.AuthorID,
ChatId: tc.ChatID,
Text: tc.Text,
Id: tc.ID,
}, &rsp)
assert.Equal(t, tc.Error, err)
if tc.Error != nil {
assert.Nil(t, rsp.Message)
return
}
assertMessagesMatch(t, &pb.Message{
AuthorId: tc.AuthorID,
ChatId: tc.ChatID,
SentAt: h.Time().Format(time.RFC3339Nano),
Text: tc.Text,
Id: tc.ID,
}, rsp.Message)
})
}
}

View File

@@ -1,69 +0,0 @@
package handler
import (
"context"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/micro/v3/service/store"
pb "github.com/micro/services/chats/proto"
)
const DefaultLimit = 25
// List the messages within a chat in reverse chronological order, using sent_before to
// offset as older messages need to be loaded
func (c *Chats) ListMessages(ctx context.Context, req *pb.ListMessagesRequest, rsp *pb.ListMessagesResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.ChatId) == 0 {
return ErrMissingChatID
}
message := &Message{
ChatID: req.ChatId,
}
// default order is descending
order := store.OrderDesc
if req.Order == "asc" {
order = store.OrderAsc
}
opts := []store.ReadOption{
store.ReadPrefix(),
store.ReadOrder(order),
}
if req.Limit > 0 {
opts = append(opts, store.ReadLimit(uint(req.Limit)))
} else {
opts = append(opts, store.ReadLimit(uint(DefaultLimit)))
}
if req.Offset > 0 {
opts = append(opts, store.ReadOffset(uint(req.Offset)))
}
// read all the records with the chat ID suffix
recs, err := store.Read(message.Index(ctx), opts...)
if err != nil {
logger.Errorf("Error reading messages: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to database")
}
// return all the messages
for _, rec := range recs {
m := &Message{}
rec.Decode(&m)
if len(m.ID) == 0 || m.ChatID != req.ChatId {
continue
}
rsp.Messages = append(rsp.Messages, m.Serialize())
}
return nil
}

View File

@@ -1,114 +0,0 @@
package handler_test
import (
"sort"
"strconv"
"testing"
"time"
"github.com/google/uuid"
"github.com/micro/services/chats/handler"
pb "github.com/micro/services/chats/proto"
"github.com/stretchr/testify/assert"
)
func TestListMessages(t *testing.T) {
h := testHandler(t)
h.Time = time.Now
// seed some data
var chatRsp pb.CreateChatResponse
err := h.CreateChat(microAccountCtx(), &pb.CreateChatRequest{
UserIds: []string{uuid.New().String(), uuid.New().String()},
}, &chatRsp)
assert.NoError(t, err)
if err != nil {
return
}
msgs := make([]*pb.Message, 50)
for i := 0; i < len(msgs); i++ {
var rsp pb.SendMessageResponse
err := h.SendMessage(microAccountCtx(), &pb.SendMessageRequest{
ChatId: chatRsp.Chat.Id,
AuthorId: uuid.New().String(),
Text: strconv.Itoa(i),
}, &rsp)
assert.NoError(t, err)
msgs[i] = rsp.Message
}
t.Run("MissingChatID", func(t *testing.T) {
var rsp pb.ListMessagesResponse
err := h.ListMessages(microAccountCtx(), &pb.ListMessagesRequest{}, &rsp)
assert.Equal(t, handler.ErrMissingChatID, err)
assert.Nil(t, rsp.Messages)
})
t.Run("NoOffset", func(t *testing.T) {
var rsp pb.ListMessagesResponse
err := h.ListMessages(microAccountCtx(), &pb.ListMessagesRequest{
ChatId: chatRsp.Chat.Id,
}, &rsp)
assert.NoError(t, err)
if len(rsp.Messages) != handler.DefaultLimit {
t.Fatalf("Expected %v messages but got %v", handler.DefaultLimit, len(rsp.Messages))
return
}
expected := msgs[25:]
sortMessages(rsp.Messages)
for i, msg := range rsp.Messages {
assertMessagesMatch(t, expected[i], msg)
}
})
t.Run("LimitSet", func(t *testing.T) {
var rsp pb.ListMessagesResponse
err := h.ListMessages(microAccountCtx(), &pb.ListMessagesRequest{
ChatId: chatRsp.Chat.Id,
Limit: 10,
}, &rsp)
assert.NoError(t, err)
if len(rsp.Messages) != 10 {
t.Fatalf("Expected %v messages but got %v", 10, len(rsp.Messages))
return
}
expected := msgs[40:]
sortMessages(rsp.Messages)
for i, msg := range rsp.Messages {
assertMessagesMatch(t, expected[i], msg)
}
})
t.Run("OffsetAndLimit", func(t *testing.T) {
var rsp pb.ListMessagesResponse
err := h.ListMessages(microAccountCtx(), &pb.ListMessagesRequest{
ChatId: chatRsp.Chat.Id,
Limit: 5,
Offset: 15,
}, &rsp)
assert.NoError(t, err)
if len(rsp.Messages) != 5 {
t.Fatalf("Expected %v messages but got %v", 5, len(rsp.Messages))
return
}
expected := msgs[30:35]
sortMessages(rsp.Messages)
for i, msg := range rsp.Messages {
assertMessagesMatch(t, expected[i], msg)
}
})
}
// sortMessages by the time they were sent
func sortMessages(msgs []*pb.Message) {
sort.Slice(msgs, func(i, j int) bool {
if len(msgs[i].SentAt) == 0 || len(msgs[j].SentAt) == 0 {
return true
}
return handler.ParseTime(msgs[i].SentAt).Before(handler.ParseTime(msgs[j].SentAt))
})
}

View File

@@ -1,28 +0,0 @@
package main
import (
"time"
"github.com/micro/services/chats/handler"
pb "github.com/micro/services/chats/proto"
"github.com/micro/micro/v3/service"
"github.com/micro/micro/v3/service/logger"
)
func main() {
// Create service
srv := service.New(
service.Name("chats"),
service.Version("latest"),
)
h := &handler.Chats{Time: time.Now}
// Register handler
pb.RegisterChatsHandler(srv.Server(), h)
// Run service
if err := srv.Run(); err != nil {
logger.Fatal(err)
}
}

View File

@@ -1 +0,0 @@
service chats

View File

@@ -1,746 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v3.15.6
// source: proto/chats.proto
package chats
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Chat struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// unique id of the chat
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
// list of users in the chat
UserIds []string `protobuf:"bytes,2,rep,name=user_ids,json=userIds,proto3" json:"user_ids,omitempty"`
// RFC3339Nano timestamp
CreatedAt string `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
}
func (x *Chat) Reset() {
*x = Chat{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_chats_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Chat) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Chat) ProtoMessage() {}
func (x *Chat) ProtoReflect() protoreflect.Message {
mi := &file_proto_chats_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Chat.ProtoReflect.Descriptor instead.
func (*Chat) Descriptor() ([]byte, []int) {
return file_proto_chats_proto_rawDescGZIP(), []int{0}
}
func (x *Chat) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *Chat) GetUserIds() []string {
if x != nil {
return x.UserIds
}
return nil
}
func (x *Chat) GetCreatedAt() string {
if x != nil {
return x.CreatedAt
}
return ""
}
type Message struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// unique id of the message
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
// user id of the message
AuthorId string `protobuf:"bytes,2,opt,name=author_id,json=authorId,proto3" json:"author_id,omitempty"`
// chat id the message belongs to
ChatId string `protobuf:"bytes,3,opt,name=chat_id,json=chatId,proto3" json:"chat_id,omitempty"`
// text within the message
Text string `protobuf:"bytes,4,opt,name=text,proto3" json:"text,omitempty"`
// RFC3339Nano timestamp
SentAt string `protobuf:"bytes,5,opt,name=sent_at,json=sentAt,proto3" json:"sent_at,omitempty"`
}
func (x *Message) Reset() {
*x = Message{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_chats_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Message) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Message) ProtoMessage() {}
func (x *Message) ProtoReflect() protoreflect.Message {
mi := &file_proto_chats_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Message.ProtoReflect.Descriptor instead.
func (*Message) Descriptor() ([]byte, []int) {
return file_proto_chats_proto_rawDescGZIP(), []int{1}
}
func (x *Message) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *Message) GetAuthorId() string {
if x != nil {
return x.AuthorId
}
return ""
}
func (x *Message) GetChatId() string {
if x != nil {
return x.ChatId
}
return ""
}
func (x *Message) GetText() string {
if x != nil {
return x.Text
}
return ""
}
func (x *Message) GetSentAt() string {
if x != nil {
return x.SentAt
}
return ""
}
// Create a new chat between mulitple users
type CreateChatRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The chat ID
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
// List of users in the chat
UserIds []string `protobuf:"bytes,2,rep,name=user_ids,json=userIds,proto3" json:"user_ids,omitempty"`
}
func (x *CreateChatRequest) Reset() {
*x = CreateChatRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_chats_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreateChatRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateChatRequest) ProtoMessage() {}
func (x *CreateChatRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_chats_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateChatRequest.ProtoReflect.Descriptor instead.
func (*CreateChatRequest) Descriptor() ([]byte, []int) {
return file_proto_chats_proto_rawDescGZIP(), []int{2}
}
func (x *CreateChatRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *CreateChatRequest) GetUserIds() []string {
if x != nil {
return x.UserIds
}
return nil
}
type CreateChatResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Chat *Chat `protobuf:"bytes,1,opt,name=chat,proto3" json:"chat,omitempty"`
}
func (x *CreateChatResponse) Reset() {
*x = CreateChatResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_chats_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreateChatResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateChatResponse) ProtoMessage() {}
func (x *CreateChatResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_chats_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateChatResponse.ProtoReflect.Descriptor instead.
func (*CreateChatResponse) Descriptor() ([]byte, []int) {
return file_proto_chats_proto_rawDescGZIP(), []int{3}
}
func (x *CreateChatResponse) GetChat() *Chat {
if x != nil {
return x.Chat
}
return nil
}
// Send a message to a chat room
type SendMessageRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
ChatId string `protobuf:"bytes,2,opt,name=chat_id,json=chatId,proto3" json:"chat_id,omitempty"`
AuthorId string `protobuf:"bytes,3,opt,name=author_id,json=authorId,proto3" json:"author_id,omitempty"`
Text string `protobuf:"bytes,4,opt,name=text,proto3" json:"text,omitempty"`
}
func (x *SendMessageRequest) Reset() {
*x = SendMessageRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_chats_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SendMessageRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SendMessageRequest) ProtoMessage() {}
func (x *SendMessageRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_chats_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SendMessageRequest.ProtoReflect.Descriptor instead.
func (*SendMessageRequest) Descriptor() ([]byte, []int) {
return file_proto_chats_proto_rawDescGZIP(), []int{4}
}
func (x *SendMessageRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *SendMessageRequest) GetChatId() string {
if x != nil {
return x.ChatId
}
return ""
}
func (x *SendMessageRequest) GetAuthorId() string {
if x != nil {
return x.AuthorId
}
return ""
}
func (x *SendMessageRequest) GetText() string {
if x != nil {
return x.Text
}
return ""
}
type SendMessageResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message *Message `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
func (x *SendMessageResponse) Reset() {
*x = SendMessageResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_chats_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SendMessageResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SendMessageResponse) ProtoMessage() {}
func (x *SendMessageResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_chats_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SendMessageResponse.ProtoReflect.Descriptor instead.
func (*SendMessageResponse) Descriptor() ([]byte, []int) {
return file_proto_chats_proto_rawDescGZIP(), []int{5}
}
func (x *SendMessageResponse) GetMessage() *Message {
if x != nil {
return x.Message
}
return nil
}
// List messages within a chat
type ListMessagesRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// unique id of the chat
ChatId string `protobuf:"bytes,1,opt,name=chat_id,json=chatId,proto3" json:"chat_id,omitempty"`
// limit the number of messages
Limit int64 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"`
// offset for the messages
Offset int64 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"`
// order "asc" or "desc" (defaults to reverse chronological)
Order string `protobuf:"bytes,4,opt,name=order,proto3" json:"order,omitempty"`
}
func (x *ListMessagesRequest) Reset() {
*x = ListMessagesRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_chats_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListMessagesRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListMessagesRequest) ProtoMessage() {}
func (x *ListMessagesRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_chats_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListMessagesRequest.ProtoReflect.Descriptor instead.
func (*ListMessagesRequest) Descriptor() ([]byte, []int) {
return file_proto_chats_proto_rawDescGZIP(), []int{6}
}
func (x *ListMessagesRequest) GetChatId() string {
if x != nil {
return x.ChatId
}
return ""
}
func (x *ListMessagesRequest) GetLimit() int64 {
if x != nil {
return x.Limit
}
return 0
}
func (x *ListMessagesRequest) GetOffset() int64 {
if x != nil {
return x.Offset
}
return 0
}
func (x *ListMessagesRequest) GetOrder() string {
if x != nil {
return x.Order
}
return ""
}
type ListMessagesResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Messages []*Message `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"`
}
func (x *ListMessagesResponse) Reset() {
*x = ListMessagesResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_chats_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListMessagesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListMessagesResponse) ProtoMessage() {}
func (x *ListMessagesResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_chats_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListMessagesResponse.ProtoReflect.Descriptor instead.
func (*ListMessagesResponse) Descriptor() ([]byte, []int) {
return file_proto_chats_proto_rawDescGZIP(), []int{7}
}
func (x *ListMessagesResponse) GetMessages() []*Message {
if x != nil {
return x.Messages
}
return nil
}
var File_proto_chats_proto protoreflect.FileDescriptor
var file_proto_chats_proto_rawDesc = []byte{
0x0a, 0x11, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x68, 0x61, 0x74, 0x73, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x12, 0x05, 0x63, 0x68, 0x61, 0x74, 0x73, 0x22, 0x50, 0x0a, 0x04, 0x43, 0x68,
0x61, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
0x69, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02,
0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x73, 0x12, 0x1d, 0x0a,
0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
0x09, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x7c, 0x0a, 0x07,
0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f,
0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x75, 0x74, 0x68,
0x6f, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18,
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x68, 0x61, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a,
0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78,
0x74, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01,
0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x6e, 0x74, 0x41, 0x74, 0x22, 0x3e, 0x0a, 0x11, 0x43, 0x72,
0x65, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
0x19, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
0x09, 0x52, 0x07, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x73, 0x22, 0x35, 0x0a, 0x12, 0x43, 0x72,
0x65, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x1f, 0x0a, 0x04, 0x63, 0x68, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b,
0x2e, 0x63, 0x68, 0x61, 0x74, 0x73, 0x2e, 0x43, 0x68, 0x61, 0x74, 0x52, 0x04, 0x63, 0x68, 0x61,
0x74, 0x22, 0x6e, 0x0a, 0x12, 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x74, 0x5f,
0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x68, 0x61, 0x74, 0x49, 0x64,
0x12, 0x1b, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x49, 0x64, 0x12, 0x12, 0x0a,
0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78,
0x74, 0x22, 0x3f, 0x0a, 0x13, 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73,
0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x63, 0x68, 0x61, 0x74,
0x73, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x22, 0x72, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x63, 0x68, 0x61,
0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x68, 0x61, 0x74,
0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73,
0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74,
0x12, 0x14, 0x0a, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x42, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a,
0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
0x32, 0x0e, 0x2e, 0x63, 0x68, 0x61, 0x74, 0x73, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x32, 0xd9, 0x01, 0x0a, 0x05, 0x43,
0x68, 0x61, 0x74, 0x73, 0x12, 0x41, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x68,
0x61, 0x74, 0x12, 0x18, 0x2e, 0x63, 0x68, 0x61, 0x74, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74,
0x65, 0x43, 0x68, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63,
0x68, 0x61, 0x74, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x74, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x4d,
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x19, 0x2e, 0x63, 0x68, 0x61, 0x74, 0x73, 0x2e, 0x53,
0x65, 0x6e, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x1a, 0x2e, 0x63, 0x68, 0x61, 0x74, 0x73, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a,
0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x12, 0x1a, 0x2e,
0x63, 0x68, 0x61, 0x74, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x63, 0x68, 0x61, 0x74,
0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x0f, 0x5a, 0x0d, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x3b, 0x63, 0x68, 0x61, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_proto_chats_proto_rawDescOnce sync.Once
file_proto_chats_proto_rawDescData = file_proto_chats_proto_rawDesc
)
func file_proto_chats_proto_rawDescGZIP() []byte {
file_proto_chats_proto_rawDescOnce.Do(func() {
file_proto_chats_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_chats_proto_rawDescData)
})
return file_proto_chats_proto_rawDescData
}
var file_proto_chats_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_proto_chats_proto_goTypes = []interface{}{
(*Chat)(nil), // 0: chats.Chat
(*Message)(nil), // 1: chats.Message
(*CreateChatRequest)(nil), // 2: chats.CreateChatRequest
(*CreateChatResponse)(nil), // 3: chats.CreateChatResponse
(*SendMessageRequest)(nil), // 4: chats.SendMessageRequest
(*SendMessageResponse)(nil), // 5: chats.SendMessageResponse
(*ListMessagesRequest)(nil), // 6: chats.ListMessagesRequest
(*ListMessagesResponse)(nil), // 7: chats.ListMessagesResponse
}
var file_proto_chats_proto_depIdxs = []int32{
0, // 0: chats.CreateChatResponse.chat:type_name -> chats.Chat
1, // 1: chats.SendMessageResponse.message:type_name -> chats.Message
1, // 2: chats.ListMessagesResponse.messages:type_name -> chats.Message
2, // 3: chats.Chats.CreateChat:input_type -> chats.CreateChatRequest
4, // 4: chats.Chats.SendMessage:input_type -> chats.SendMessageRequest
6, // 5: chats.Chats.ListMessages:input_type -> chats.ListMessagesRequest
3, // 6: chats.Chats.CreateChat:output_type -> chats.CreateChatResponse
5, // 7: chats.Chats.SendMessage:output_type -> chats.SendMessageResponse
7, // 8: chats.Chats.ListMessages:output_type -> chats.ListMessagesResponse
6, // [6:9] is the sub-list for method output_type
3, // [3:6] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_proto_chats_proto_init() }
func file_proto_chats_proto_init() {
if File_proto_chats_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_proto_chats_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Chat); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_chats_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Message); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_chats_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CreateChatRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_chats_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CreateChatResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_chats_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SendMessageRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_chats_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SendMessageResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_chats_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListMessagesRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_chats_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListMessagesResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_chats_proto_rawDesc,
NumEnums: 0,
NumMessages: 8,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_proto_chats_proto_goTypes,
DependencyIndexes: file_proto_chats_proto_depIdxs,
MessageInfos: file_proto_chats_proto_msgTypes,
}.Build()
File_proto_chats_proto = out.File
file_proto_chats_proto_rawDesc = nil
file_proto_chats_proto_goTypes = nil
file_proto_chats_proto_depIdxs = nil
}

View File

@@ -1,137 +0,0 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: proto/chats.proto
package chats
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
import (
context "context"
api "github.com/micro/micro/v3/service/api"
client "github.com/micro/micro/v3/service/client"
server "github.com/micro/micro/v3/service/server"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
// Reference imports to suppress errors if they are not otherwise used.
var _ api.Endpoint
var _ context.Context
var _ client.Option
var _ server.Option
// Api Endpoints for Chats service
func NewChatsEndpoints() []*api.Endpoint {
return []*api.Endpoint{}
}
// Client API for Chats service
type ChatsService interface {
// Create a chat between two or more users, if a chat already exists for these users, the existing
// chat will be returned
CreateChat(ctx context.Context, in *CreateChatRequest, opts ...client.CallOption) (*CreateChatResponse, error)
// Create a message within a chat
SendMessage(ctx context.Context, in *SendMessageRequest, opts ...client.CallOption) (*SendMessageResponse, error)
// List the messages within a chat in reverse chronological order, using sent_before to
// offset as older messages need to be loaded
ListMessages(ctx context.Context, in *ListMessagesRequest, opts ...client.CallOption) (*ListMessagesResponse, error)
}
type chatsService struct {
c client.Client
name string
}
func NewChatsService(name string, c client.Client) ChatsService {
return &chatsService{
c: c,
name: name,
}
}
func (c *chatsService) CreateChat(ctx context.Context, in *CreateChatRequest, opts ...client.CallOption) (*CreateChatResponse, error) {
req := c.c.NewRequest(c.name, "Chats.CreateChat", in)
out := new(CreateChatResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *chatsService) SendMessage(ctx context.Context, in *SendMessageRequest, opts ...client.CallOption) (*SendMessageResponse, error) {
req := c.c.NewRequest(c.name, "Chats.SendMessage", in)
out := new(SendMessageResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *chatsService) ListMessages(ctx context.Context, in *ListMessagesRequest, opts ...client.CallOption) (*ListMessagesResponse, error) {
req := c.c.NewRequest(c.name, "Chats.ListMessages", in)
out := new(ListMessagesResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Chats service
type ChatsHandler interface {
// Create a chat between two or more users, if a chat already exists for these users, the existing
// chat will be returned
CreateChat(context.Context, *CreateChatRequest, *CreateChatResponse) error
// Create a message within a chat
SendMessage(context.Context, *SendMessageRequest, *SendMessageResponse) error
// List the messages within a chat in reverse chronological order, using sent_before to
// offset as older messages need to be loaded
ListMessages(context.Context, *ListMessagesRequest, *ListMessagesResponse) error
}
func RegisterChatsHandler(s server.Server, hdlr ChatsHandler, opts ...server.HandlerOption) error {
type chats interface {
CreateChat(ctx context.Context, in *CreateChatRequest, out *CreateChatResponse) error
SendMessage(ctx context.Context, in *SendMessageRequest, out *SendMessageResponse) error
ListMessages(ctx context.Context, in *ListMessagesRequest, out *ListMessagesResponse) error
}
type Chats struct {
chats
}
h := &chatsHandler{hdlr}
return s.Handle(s.NewHandler(&Chats{h}, opts...))
}
type chatsHandler struct {
ChatsHandler
}
func (h *chatsHandler) CreateChat(ctx context.Context, in *CreateChatRequest, out *CreateChatResponse) error {
return h.ChatsHandler.CreateChat(ctx, in, out)
}
func (h *chatsHandler) SendMessage(ctx context.Context, in *SendMessageRequest, out *SendMessageResponse) error {
return h.ChatsHandler.SendMessage(ctx, in, out)
}
func (h *chatsHandler) ListMessages(ctx context.Context, in *ListMessagesRequest, out *ListMessagesResponse) error {
return h.ChatsHandler.ListMessages(ctx, in, out)
}

View File

@@ -1,78 +0,0 @@
syntax = "proto3";
package chats;
option go_package = "./proto;chats";
service Chats {
// Create a chat between two or more users, if a chat already exists for these users, the existing
// chat will be returned
rpc CreateChat(CreateChatRequest) returns (CreateChatResponse);
// Create a message within a chat
rpc SendMessage(SendMessageRequest) returns (SendMessageResponse);
// List the messages within a chat in reverse chronological order, using sent_before to
// offset as older messages need to be loaded
rpc ListMessages(ListMessagesRequest) returns (ListMessagesResponse);
}
message Chat {
// unique id of the chat
string id = 1;
// list of users in the chat
repeated string user_ids = 2;
// RFC3339 Nano timestamp e.g 2006-01-02T15:04:05.999999999Z07:00
string created_at = 3;
}
message Message {
// unique id of the message
string id = 1;
// user id of the message
string author_id = 2;
// chat id the message belongs to
string chat_id = 3;
// text within the message
string text = 4;
// RFC3339 Nano timestamp e.g 2006-01-02T15:04:05.999999999Z07:00
string sent_at = 5;
}
// Create a new chat between mulitple users
message CreateChatRequest {
// The chat ID
string id = 1;
// List of users in the chat
repeated string user_ids = 2;
}
message CreateChatResponse {
Chat chat = 1;
}
// Send a message to a chat room
message SendMessageRequest {
string id = 1;
string chat_id = 2;
string author_id = 3;
string text = 4;
}
message SendMessageResponse {
Message message = 1;
}
// List messages within a chat
message ListMessagesRequest {
// unique id of the chat
string chat_id = 1;
// limit the number of messages
int64 limit = 2;
// offset for the messages
int64 offset = 3;
// order "asc" or "desc" (defaults to reverse chronological)
string order = 4;
}
message ListMessagesResponse {
repeated Message messages = 1;
}

2
groups/.gitignore vendored
View File

@@ -1,2 +0,0 @@
groups

View File

@@ -1,3 +0,0 @@
FROM alpine
ADD groups /groups
ENTRYPOINT [ "/groups" ]

View File

@@ -1,27 +0,0 @@
GOPATH:=$(shell go env GOPATH)
.PHONY: init
init:
go get -u github.com/golang/protobuf/proto
go get -u github.com/golang/protobuf/protoc-gen-go
go get github.com/micro/micro/v3/cmd/protoc-gen-micro
.PHONY: proto
proto:
protoc --openapi_out=. --proto_path=. --micro_out=. --go_out=:. proto/groups.proto
.PHONY: docs
docs:
protoc --openapi_out=. --proto_path=. --micro_out=. --go_out=:. proto/groups.proto
@redoc-cli bundle api-groups.json
.PHONY: build
build:
go build -o groups *.go
.PHONY: test
test:
go test -v ./... -cover
.PHONY: docker
docker:
docker build . -t groups:latest

View File

@@ -1,6 +0,0 @@
Manage group memberships
# Groups Service
The group service is a basic CRUD service for group membership management. Create groups, add members and lookup which groups a user is a member of.

View File

@@ -1,3 +0,0 @@
package main
//go:generate make proto

View File

@@ -1,375 +0,0 @@
package handler
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/store"
pb "github.com/micro/services/groups/proto"
"github.com/micro/services/pkg/tenant"
)
var (
ErrMissingName = errors.BadRequest("MISSING_NAME", "Missing name")
ErrMissingID = errors.BadRequest("MISSING_ID", "Missing ID")
ErrMissingIDs = errors.BadRequest("MISSING_IDS", "One or more IDs are required")
ErrMissingGroupID = errors.BadRequest("MISSING_GROUP_ID", "Missing Group ID")
ErrMissingMemberID = errors.BadRequest("MISSING_MEMBER_ID", "Missing Member ID")
ErrNotFound = errors.BadRequest("NOT_FOUND", "No group found with this ID")
ErrStore = errors.InternalServerError("STORE_ERROR", "Error connecting to the store")
)
type Group struct {
ID string
Name string
Members []string
}
type Member struct {
ID string
Group string
}
func (g *Group) Key(ctx context.Context) string {
key := fmt.Sprintf("group:%s", g.ID)
t, ok := tenant.FromContext(ctx)
if !ok {
return key
}
return fmt.Sprintf("%s/%s", t, key)
}
func (m *Member) Key(ctx context.Context) string {
key := fmt.Sprintf("member:%s:%s", m.ID, m.Group)
t, ok := tenant.FromContext(ctx)
if !ok {
return key
}
return fmt.Sprintf("%s/%s", t, key)
}
func (g *Group) Serialize() *pb.Group {
memberIDs := make([]string, len(g.Members))
for i, m := range g.Members {
memberIDs[i] = m
}
return &pb.Group{Id: g.ID, Name: g.Name, MemberIds: memberIDs}
}
type Groups struct{}
func (g *Groups) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.Name) == 0 {
return ErrMissingName
}
// create the group object
group := &Group{ID: uuid.New().String(), Name: req.Name}
// write the group record
if err := store.Write(store.NewRecord(group.Key(ctx), group)); err != nil {
return ErrStore
}
// return the group
rsp.Group = group.Serialize()
return nil
}
func (g *Groups) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.Ids) == 0 {
return ErrMissingIDs
}
// serialize the response
rsp.Groups = make(map[string]*pb.Group)
for _, id := range req.Ids {
group := &Group{
ID: id,
}
recs, err := store.Read(group.Key(ctx), store.ReadLimit(1))
if err != nil {
return ErrStore
}
if len(recs) == 0 {
continue
}
if err := recs[0].Decode(&group); err != nil {
continue
}
rsp.Groups[group.ID] = group.Serialize()
}
return nil
}
func (g *Groups) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UpdateResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.Id) == 0 {
return ErrMissingID
}
if len(req.Name) == 0 {
return ErrMissingName
}
group := &Group{ID: req.Id}
recs, err := store.Read(group.Key(ctx), store.ReadLimit(1))
if err == store.ErrNotFound {
return ErrNotFound
} else if err != nil {
return ErrStore
}
// decode the record
recs[0].Decode(&group)
// set the name
group.Name = req.Name
// save the record
if err := store.Write(store.NewRecord(group.Key(ctx), group)); err != nil {
return ErrStore
}
// serialize the response
rsp.Group = group.Serialize()
return nil
}
func (g *Groups) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.DeleteResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.Id) == 0 {
return ErrMissingID
}
group := &Group{ID: req.Id}
// get the group
recs, err := store.Read(group.Key(ctx), store.ReadLimit(1))
if err == store.ErrNotFound {
return nil
} else if err != nil {
return ErrStore
}
// decode the record
recs[0].Decode(&group)
// delete the record
if err := store.Delete(group.Key(ctx)); err == store.ErrNotFound {
return nil
} else if err != nil {
return ErrStore
}
// delete all the members
for _, memberId := range group.Members {
m := &Member{
ID: memberId,
}
// delete the member
store.Delete(m.Key(ctx))
}
return nil
}
func (g *Groups) List(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
if len(req.MemberId) > 0 {
// only list groups the user is a member of
m := &Member{ID: req.MemberId}
recs, err := store.Read(m.Key(ctx), store.ReadPrefix())
if err != nil {
return ErrStore
}
for _, rec := range recs {
m := &Member{ID: req.MemberId}
rec.Decode(&m)
// get the group
group := &Group{ID: m.Group}
grecs, err := store.Read(group.Key(ctx), store.ReadLimit(1))
if err != nil {
return ErrStore
}
grecs[0].Decode(&group)
rsp.Groups = append(rsp.Groups, group.Serialize())
}
return nil
}
group := &Group{}
// read all the prefixes
recs, err := store.Read(group.Key(ctx), store.ReadPrefix())
if err != nil {
return ErrStore
}
// serialize and return response
for _, rec := range recs {
group := new(Group)
rec.Decode(&group)
rsp.Groups = append(rsp.Groups, group.Serialize())
}
return nil
}
func (g *Groups) AddMember(ctx context.Context, req *pb.AddMemberRequest, rsp *pb.AddMemberResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.GroupId) == 0 {
return ErrMissingGroupID
}
if len(req.MemberId) == 0 {
return ErrMissingMemberID
}
// read the group
group := &Group{ID: req.GroupId}
recs, err := store.Read(group.Key(ctx), store.ReadLimit(1))
if err == store.ErrNotFound {
return ErrNotFound
} else if err != nil {
return ErrStore
}
// decode the record
recs[0].Decode(group)
var seen bool
for _, member := range group.Members {
if member == req.MemberId {
seen = true
break
}
}
// already a member
if seen {
return nil
}
// add the member
group.Members = append(group.Members, req.MemberId)
// save the record
if err := store.Write(store.NewRecord(group.Key(ctx), group)); err != nil {
return ErrStore
}
// add the member record
m := &Member{
ID: req.MemberId,
Group: group.ID,
}
// write the record
if err := store.Write(store.NewRecord(m.Key(ctx), m)); err != nil {
return ErrStore
}
return nil
}
func (g *Groups) RemoveMember(ctx context.Context, req *pb.RemoveMemberRequest, rsp *pb.RemoveMemberResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.GroupId) == 0 {
return ErrMissingGroupID
}
if len(req.MemberId) == 0 {
return ErrMissingMemberID
}
// read the group
group := &Group{ID: req.GroupId}
// read the gruop
recs, err := store.Read(group.Key(ctx), store.ReadLimit(1))
if err == store.ErrNotFound {
return ErrNotFound
} else if err != nil {
return ErrStore
}
// decode the record
recs[0].Decode(&group)
// new member id list
var members []string
for _, member := range group.Members {
if member == req.MemberId {
continue
}
members = append(members, member)
}
// update the member
group.Members = members
// save the record
if err := store.Write(store.NewRecord(group.Key(ctx), group)); err != nil {
return ErrStore
}
// delete the member
m := &Member{
ID: req.MemberId,
Group: group.ID,
}
if err := store.Delete(m.Key(ctx)); err != nil {
return ErrStore
}
return nil
}

View File

@@ -1,280 +0,0 @@
package handler_test
import (
"context"
"sort"
"testing"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/store"
"github.com/micro/micro/v3/service/store/memory"
"github.com/micro/services/groups/handler"
pb "github.com/micro/services/groups/proto"
"github.com/stretchr/testify/assert"
)
func testHandler(t *testing.T) *handler.Groups {
store.DefaultStore = memory.NewStore()
return &handler.Groups{}
}
func TestCreate(t *testing.T) {
h := testHandler(t)
t.Run("MissingName", func(t *testing.T) {
err := h.Create(microAccountCtx(), &pb.CreateRequest{}, &pb.CreateResponse{})
assert.Equal(t, handler.ErrMissingName, err)
})
t.Run("Valid", func(t *testing.T) {
err := h.Create(microAccountCtx(), &pb.CreateRequest{
Name: "Doe Family Group",
}, &pb.CreateResponse{})
assert.NoError(t, err)
})
}
func TestUpdate(t *testing.T) {
h := testHandler(t)
t.Run("MissingID", func(t *testing.T) {
err := h.Update(microAccountCtx(), &pb.UpdateRequest{
Name: "Doe Family Group",
}, &pb.UpdateResponse{})
assert.Equal(t, handler.ErrMissingID, err)
})
t.Run("MissingName", func(t *testing.T) {
err := h.Update(microAccountCtx(), &pb.UpdateRequest{
Id: uuid.New().String(),
}, &pb.UpdateResponse{})
assert.Equal(t, handler.ErrMissingName, err)
})
t.Run("NotFound", func(t *testing.T) {
err := h.Update(microAccountCtx(), &pb.UpdateRequest{
Id: uuid.New().String(),
Name: "Bar Family Group",
}, &pb.UpdateResponse{})
assert.Equal(t, handler.ErrNotFound, err)
})
t.Run("Valid", func(t *testing.T) {
// create a demo group
var cRsp pb.CreateResponse
err := h.Create(microAccountCtx(), &pb.CreateRequest{
Name: "Doe Family Group",
}, &cRsp)
assert.NoError(t, err)
err = h.Update(microAccountCtx(), &pb.UpdateRequest{
Id: cRsp.Group.Id,
Name: "Bar Family Group",
}, &pb.UpdateResponse{})
assert.NoError(t, err)
var rRsp pb.ReadResponse
err = h.Read(microAccountCtx(), &pb.ReadRequest{
Ids: []string{cRsp.Group.Id},
}, &rRsp)
assert.NoError(t, err)
g := rRsp.Groups[cRsp.Group.Id]
if g == nil {
t.Errorf("Group not returned")
} else {
assert.Equal(t, "Bar Family Group", g.Name)
}
})
}
func TestDelete(t *testing.T) {
h := testHandler(t)
t.Run("MissingID", func(t *testing.T) {
err := h.Delete(microAccountCtx(), &pb.DeleteRequest{}, &pb.DeleteResponse{})
assert.Equal(t, handler.ErrMissingID, err)
})
t.Run("NotFound", func(t *testing.T) {
err := h.Delete(microAccountCtx(), &pb.DeleteRequest{
Id: uuid.New().String(),
}, &pb.DeleteResponse{})
assert.NoError(t, err)
})
// create a demo group
var cRsp pb.CreateResponse
err := h.Create(microAccountCtx(), &pb.CreateRequest{
Name: "Doe Family Group",
}, &cRsp)
assert.NoError(t, err)
t.Run("Valid", func(t *testing.T) {
err := h.Delete(microAccountCtx(), &pb.DeleteRequest{
Id: cRsp.Group.Id,
}, &pb.DeleteResponse{})
assert.NoError(t, err)
var rRsp pb.ReadResponse
err = h.Read(microAccountCtx(), &pb.ReadRequest{
Ids: []string{cRsp.Group.Id},
}, &rRsp)
assert.Nil(t, rRsp.Groups[cRsp.Group.Id])
})
}
func TestList(t *testing.T) {
h := testHandler(t)
// create two demo groups
var cRsp1 pb.CreateResponse
err := h.Create(microAccountCtx(), &pb.CreateRequest{
Name: "Alpha Group",
}, &cRsp1)
assert.NoError(t, err)
var cRsp2 pb.CreateResponse
err = h.Create(microAccountCtx(), &pb.CreateRequest{
Name: "Bravo Group",
}, &cRsp2)
assert.NoError(t, err)
// add a member to the first group
uid := uuid.New().String()
err = h.AddMember(microAccountCtx(), &pb.AddMemberRequest{
GroupId: cRsp1.Group.Id, MemberId: uid,
}, &pb.AddMemberResponse{})
assert.NoError(t, err)
t.Run("Unscoped", func(t *testing.T) {
var rsp pb.ListResponse
err = h.List(microAccountCtx(), &pb.ListRequest{}, &rsp)
assert.NoError(t, err)
assert.Lenf(t, rsp.Groups, 2, "Two groups should be returned")
if len(rsp.Groups) != 2 {
return
}
sort.Slice(rsp.Groups, func(i, j int) bool {
return rsp.Groups[i].Name < rsp.Groups[j].Name
})
assert.Equal(t, cRsp1.Group.Id, rsp.Groups[0].Id)
assert.Equal(t, cRsp1.Group.Name, rsp.Groups[0].Name)
assert.Len(t, rsp.Groups[0].MemberIds, 1)
assert.Contains(t, rsp.Groups[0].MemberIds, uid)
assert.Equal(t, cRsp2.Group.Id, rsp.Groups[1].Id)
assert.Equal(t, cRsp2.Group.Name, rsp.Groups[1].Name)
assert.Len(t, rsp.Groups[1].MemberIds, 0)
})
t.Run("Scoped", func(t *testing.T) {
var rsp pb.ListResponse
err = h.List(microAccountCtx(), &pb.ListRequest{MemberId: uid}, &rsp)
assert.NoError(t, err)
assert.Lenf(t, rsp.Groups, 1, "One group should be returned")
if len(rsp.Groups) != 1 {
return
}
assert.Equal(t, cRsp1.Group.Id, rsp.Groups[0].Id)
assert.Equal(t, cRsp1.Group.Name, rsp.Groups[0].Name)
assert.Len(t, rsp.Groups[0].MemberIds, 1)
assert.Contains(t, rsp.Groups[0].MemberIds, uid)
})
}
func TestAddMember(t *testing.T) {
h := testHandler(t)
t.Run("MissingGroupID", func(t *testing.T) {
err := h.AddMember(microAccountCtx(), &pb.AddMemberRequest{
MemberId: uuid.New().String(),
}, &pb.AddMemberResponse{})
assert.Equal(t, handler.ErrMissingGroupID, err)
})
t.Run("MissingMemberID", func(t *testing.T) {
err := h.AddMember(microAccountCtx(), &pb.AddMemberRequest{
GroupId: uuid.New().String(),
}, &pb.AddMemberResponse{})
assert.Equal(t, handler.ErrMissingMemberID, err)
})
t.Run("GroupNotFound", func(t *testing.T) {
err := h.AddMember(microAccountCtx(), &pb.AddMemberRequest{
GroupId: uuid.New().String(),
MemberId: uuid.New().String(),
}, &pb.AddMemberResponse{})
assert.Equal(t, handler.ErrNotFound, err)
})
// create a test group
var cRsp pb.CreateResponse
err := h.Create(microAccountCtx(), &pb.CreateRequest{
Name: "Alpha Group",
}, &cRsp)
assert.NoError(t, err)
t.Run("Valid", func(t *testing.T) {
err := h.AddMember(microAccountCtx(), &pb.AddMemberRequest{
GroupId: cRsp.Group.Id,
MemberId: uuid.New().String(),
}, &pb.AddMemberResponse{})
assert.NoError(t, err)
})
t.Run("Retry", func(t *testing.T) {
err := h.AddMember(microAccountCtx(), &pb.AddMemberRequest{
GroupId: cRsp.Group.Id,
MemberId: uuid.New().String(),
}, &pb.AddMemberResponse{})
assert.NoError(t, err)
})
}
func TestRemoveMember(t *testing.T) {
h := testHandler(t)
t.Run("MissingGroupID", func(t *testing.T) {
err := h.RemoveMember(microAccountCtx(), &pb.RemoveMemberRequest{
MemberId: uuid.New().String(),
}, &pb.RemoveMemberResponse{})
assert.Equal(t, handler.ErrMissingGroupID, err)
})
t.Run("MissingMemberID", func(t *testing.T) {
err := h.RemoveMember(microAccountCtx(), &pb.RemoveMemberRequest{
GroupId: uuid.New().String(),
}, &pb.RemoveMemberResponse{})
assert.Equal(t, handler.ErrMissingMemberID, err)
})
// create a test group
var cRsp pb.CreateResponse
err := h.Create(microAccountCtx(), &pb.CreateRequest{
Name: "Alpha Group",
}, &cRsp)
assert.NoError(t, err)
t.Run("Valid", func(t *testing.T) {
err := h.RemoveMember(microAccountCtx(), &pb.RemoveMemberRequest{
GroupId: cRsp.Group.Id,
MemberId: uuid.New().String(),
}, &pb.RemoveMemberResponse{})
assert.NoError(t, err)
})
t.Run("Retry", func(t *testing.T) {
err := h.RemoveMember(microAccountCtx(), &pb.RemoveMemberRequest{
GroupId: cRsp.Group.Id,
MemberId: uuid.New().String(),
}, &pb.RemoveMemberResponse{})
assert.NoError(t, err)
})
}
func microAccountCtx() context.Context {
return auth.ContextWithAccount(context.TODO(), &auth.Account{
Issuer: "micro",
ID: "someID",
})
}

View File

@@ -1,24 +0,0 @@
package main
import (
"github.com/micro/micro/v3/service"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/services/groups/handler"
pb "github.com/micro/services/groups/proto"
)
func main() {
// Create service
srv := service.New(
service.Name("groups"),
service.Version("latest"),
)
// Register handler
pb.RegisterGroupsHandler(srv.Server(), new(handler.Groups))
// Run service
if err := srv.Run(); err != nil {
logger.Fatal(err)
}
}

View File

@@ -1 +0,0 @@
service groups

File diff suppressed because it is too large Load Diff

View File

@@ -1,209 +0,0 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: proto/groups.proto
package groups
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
import (
context "context"
api "github.com/micro/micro/v3/service/api"
client "github.com/micro/micro/v3/service/client"
server "github.com/micro/micro/v3/service/server"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
// Reference imports to suppress errors if they are not otherwise used.
var _ api.Endpoint
var _ context.Context
var _ client.Option
var _ server.Option
// Api Endpoints for Groups service
func NewGroupsEndpoints() []*api.Endpoint {
return []*api.Endpoint{}
}
// Client API for Groups service
type GroupsService interface {
// Create a group
Create(ctx context.Context, in *CreateRequest, opts ...client.CallOption) (*CreateResponse, error)
// Read a group using ID
Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ReadResponse, error)
// Update a groups name
Update(ctx context.Context, in *UpdateRequest, opts ...client.CallOption) (*UpdateResponse, error)
// Delete a group
Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error)
// List all groups
List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error)
// AddMember to a group
AddMember(ctx context.Context, in *AddMemberRequest, opts ...client.CallOption) (*AddMemberResponse, error)
// RemoveMember from a group
RemoveMember(ctx context.Context, in *RemoveMemberRequest, opts ...client.CallOption) (*RemoveMemberResponse, error)
}
type groupsService struct {
c client.Client
name string
}
func NewGroupsService(name string, c client.Client) GroupsService {
return &groupsService{
c: c,
name: name,
}
}
func (c *groupsService) Create(ctx context.Context, in *CreateRequest, opts ...client.CallOption) (*CreateResponse, error) {
req := c.c.NewRequest(c.name, "Groups.Create", in)
out := new(CreateResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *groupsService) Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ReadResponse, error) {
req := c.c.NewRequest(c.name, "Groups.Read", in)
out := new(ReadResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *groupsService) Update(ctx context.Context, in *UpdateRequest, opts ...client.CallOption) (*UpdateResponse, error) {
req := c.c.NewRequest(c.name, "Groups.Update", in)
out := new(UpdateResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *groupsService) Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error) {
req := c.c.NewRequest(c.name, "Groups.Delete", in)
out := new(DeleteResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *groupsService) List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error) {
req := c.c.NewRequest(c.name, "Groups.List", in)
out := new(ListResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *groupsService) AddMember(ctx context.Context, in *AddMemberRequest, opts ...client.CallOption) (*AddMemberResponse, error) {
req := c.c.NewRequest(c.name, "Groups.AddMember", in)
out := new(AddMemberResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *groupsService) RemoveMember(ctx context.Context, in *RemoveMemberRequest, opts ...client.CallOption) (*RemoveMemberResponse, error) {
req := c.c.NewRequest(c.name, "Groups.RemoveMember", in)
out := new(RemoveMemberResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Groups service
type GroupsHandler interface {
// Create a group
Create(context.Context, *CreateRequest, *CreateResponse) error
// Read a group using ID
Read(context.Context, *ReadRequest, *ReadResponse) error
// Update a groups name
Update(context.Context, *UpdateRequest, *UpdateResponse) error
// Delete a group
Delete(context.Context, *DeleteRequest, *DeleteResponse) error
// List all groups
List(context.Context, *ListRequest, *ListResponse) error
// AddMember to a group
AddMember(context.Context, *AddMemberRequest, *AddMemberResponse) error
// RemoveMember from a group
RemoveMember(context.Context, *RemoveMemberRequest, *RemoveMemberResponse) error
}
func RegisterGroupsHandler(s server.Server, hdlr GroupsHandler, opts ...server.HandlerOption) error {
type groups interface {
Create(ctx context.Context, in *CreateRequest, out *CreateResponse) error
Read(ctx context.Context, in *ReadRequest, out *ReadResponse) error
Update(ctx context.Context, in *UpdateRequest, out *UpdateResponse) error
Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error
List(ctx context.Context, in *ListRequest, out *ListResponse) error
AddMember(ctx context.Context, in *AddMemberRequest, out *AddMemberResponse) error
RemoveMember(ctx context.Context, in *RemoveMemberRequest, out *RemoveMemberResponse) error
}
type Groups struct {
groups
}
h := &groupsHandler{hdlr}
return s.Handle(s.NewHandler(&Groups{h}, opts...))
}
type groupsHandler struct {
GroupsHandler
}
func (h *groupsHandler) Create(ctx context.Context, in *CreateRequest, out *CreateResponse) error {
return h.GroupsHandler.Create(ctx, in, out)
}
func (h *groupsHandler) Read(ctx context.Context, in *ReadRequest, out *ReadResponse) error {
return h.GroupsHandler.Read(ctx, in, out)
}
func (h *groupsHandler) Update(ctx context.Context, in *UpdateRequest, out *UpdateResponse) error {
return h.GroupsHandler.Update(ctx, in, out)
}
func (h *groupsHandler) Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error {
return h.GroupsHandler.Delete(ctx, in, out)
}
func (h *groupsHandler) List(ctx context.Context, in *ListRequest, out *ListResponse) error {
return h.GroupsHandler.List(ctx, in, out)
}
func (h *groupsHandler) AddMember(ctx context.Context, in *AddMemberRequest, out *AddMemberResponse) error {
return h.GroupsHandler.AddMember(ctx, in, out)
}
func (h *groupsHandler) RemoveMember(ctx context.Context, in *RemoveMemberRequest, out *RemoveMemberResponse) error {
return h.GroupsHandler.RemoveMember(ctx, in, out)
}

View File

@@ -1,81 +0,0 @@
syntax = "proto3";
package groups;
option go_package = "./proto;groups";
service Groups {
// Create a group
rpc Create(CreateRequest) returns (CreateResponse);
// Read a group using ID
rpc Read(ReadRequest) returns (ReadResponse);
// Update a groups name
rpc Update(UpdateRequest) returns (UpdateResponse);
// Delete a group
rpc Delete(DeleteRequest) returns (DeleteResponse);
// List all groups
rpc List(ListRequest) returns (ListResponse);
// AddMember to a group
rpc AddMember(AddMemberRequest) returns (AddMemberResponse);
// RemoveMember from a group
rpc RemoveMember(RemoveMemberRequest) returns (RemoveMemberResponse);
}
message Group {
string id = 1;
string name = 2;
repeated string member_ids = 3;
}
message CreateRequest {
string name = 1;
}
message CreateResponse {
Group group = 1;
}
message ReadRequest {
repeated string ids = 1;
}
message ReadResponse {
map<string, Group> groups = 1;
}
message UpdateRequest {
string id = 1;
string name = 2;
}
message UpdateResponse {
Group group = 1;
}
message DeleteRequest {
string id = 1;
}
message DeleteResponse {}
message ListRequest {
// passing a member id will restrict the groups to that which the member is part of
string member_id = 1;
}
message ListResponse {
repeated Group groups = 1;
}
message AddMemberRequest {
string group_id = 1;
string member_id = 2;
}
message AddMemberResponse {}
message RemoveMemberRequest {
string group_id = 1;
string member_id = 2;
}
message RemoveMemberResponse {}

2
invites/.gitignore vendored
View File

@@ -1,2 +0,0 @@
invites

View File

@@ -1,3 +0,0 @@
FROM alpine
ADD invites /invites
ENTRYPOINT [ "/invites" ]

View File

@@ -1,27 +0,0 @@
GOPATH:=$(shell go env GOPATH)
.PHONY: init
init:
go get -u github.com/golang/protobuf/proto
go get -u github.com/golang/protobuf/protoc-gen-go
go get github.com/micro/micro/v3/cmd/protoc-gen-micro
.PHONY: proto
proto:
protoc --proto_path=. --micro_out=. --go_out=:. proto/invites.proto
.PHONY: docs
docs:
protoc --openapi_out=. --proto_path=. --micro_out=. --go_out=:. proto/invites.proto
@redoc-cli bundle api-invites.json
.PHONY: build
build:
go build -o invites *.go
.PHONY: test
test:
go test -v ./... -cover
.PHONY: docker
docker:
docker build . -t invites:latest

View File

@@ -1,6 +0,0 @@
Manage and send user and group invites
# Invites Service
The invites services allows you to create and manage invites for users and groups by providing invite codes.

View File

@@ -1,57 +0,0 @@
{
"create": [{
"title": "Create an invite",
"description": "Create an invite for a user for a group",
"request": {
"group_id": "mygroup",
"email": "john@doe.com"
},
"response": {
"invite": {
"id": "fb3a3552-3c7b-4a18-a1f8-08ab56940862",
"group_id": "mygroup",
"email": "john@doe.com",
"code": "86285587"
}
}
}],
"read": [{
"title": "Read an invite",
"description": "Read an invite by code",
"request": {
"code": "86285587"
},
"response": {
"invite": {
"id": "fb3a3552-3c7b-4a18-a1f8-08ab56940862",
"group_id": "mygroup",
"email": "john@doe.com",
"code": "86285587"
}
}
}],
"delete": [{
"title": "Delete an invite",
"description": "Delete an invite by id",
"request": {
"id": "fb3a3552-3c7b-4a18-a1f8-08ab56940862"
},
"response": {}
}],
"list": [{
"title": "List invites",
"description": "List invites for a group by id",
"request": {
"group_id": "mygroup"
},
"response": {
"invites": [{
"id": "fb3a3552-3c7b-4a18-a1f8-08ab56940862",
"group_id": "myawesomegroup",
"email": "john@doe.com",
"code": "86285587"
}]
}
}]
}

View File

@@ -1,3 +0,0 @@
package main
//go:generate make proto

View File

@@ -1,347 +0,0 @@
package handler
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"regexp"
"strconv"
"strings"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/micro/v3/service/store"
pb "github.com/micro/services/invites/proto"
"github.com/micro/services/pkg/tenant"
)
var (
ErrMissingID = errors.BadRequest("MISSING_ID", "Missing ID")
ErrMissingGroupID = errors.BadRequest("MISSING_GROUP_ID", "Missing GroupID")
ErrInvalidEmail = errors.BadRequest("INVALID_EMAIL", "The email provided was invalid")
ErrMissingEmail = errors.BadRequest("MISSING_EMAIL", "Missing Email")
ErrMissingIDAndCode = errors.BadRequest("ID_OR_CODE_REQUIRED", "An email address code is required to read an invite")
ErrMissingGroupIDAndEmail = errors.BadRequest("GROUP_ID_OR_EMAIL_REQUIRED", "An email address or group id is needed to list invites")
ErrInviteNotFound = errors.NotFound("NOT_FOUND", "Invite not found")
emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
)
type Invite struct {
ID string
Email string
GroupID string
Code string
}
func (i *Invite) Serialize() *pb.Invite {
return &pb.Invite{
Id: i.ID,
Email: i.Email,
GroupId: i.GroupID,
Code: i.Code,
}
}
func (i *Invite) Key(ctx context.Context) string {
key := fmt.Sprintf("invite:%s:%s", i.ID, i.Code)
t, ok := tenant.FromContext(ctx)
if !ok {
return key
}
return fmt.Sprintf("%s/%s", t, key)
}
func (i *Invite) Index(ctx context.Context) string {
key := fmt.Sprintf("group:%s:%s", i.GroupID, i.Email)
t, ok := tenant.FromContext(ctx)
if !ok {
return key
}
return fmt.Sprintf("%s/%s", t, key)
}
func (i *Invite) Marshal() []byte {
b, _ := json.Marshal(i)
return b
}
func (i *Invite) Unmarshal(b []byte) error {
return json.Unmarshal(b, &i)
}
type Invites struct{}
// schema
// Read: id/code
// List: group/email
// Create an invite
func (i *Invites) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.GroupId) == 0 {
return ErrMissingGroupID
}
if len(req.Email) == 0 {
return ErrMissingEmail
}
if !isEmailValid(req.Email) {
return ErrInvalidEmail
}
// construct the invite and write to the db
invite := &Invite{
ID: uuid.New().String(),
Code: generateCode(),
GroupID: req.GroupId,
Email: strings.ToLower(req.Email),
}
// id/val
key := invite.Key(ctx)
// get group key
gkey := invite.Index(ctx)
// TODO: Use the micro/micro/v3/service/sync interface to lock
// write the first record
if err := store.Write(store.NewRecord(key, invite)); err != nil {
logger.Errorf("Error writing to the store: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// write the group record
if err := store.Write(store.NewRecord(gkey, invite)); err != nil {
logger.Errorf("Error writing to the store: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// serialize the response
rsp.Invite = invite.Serialize()
return nil
}
// Read an invite using ID or code
func (i *Invites) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.Id) == 0 && len(req.Code) == 0 {
return ErrMissingIDAndCode
}
var recs []*store.Record
var err error
// create a pseudo invite
invite := &Invite{
ID: req.Id,
Code: req.Code,
}
if len(req.Id) > 0 && len(req.Code) > 0 {
recs, err = store.Read(invite.Key(ctx), store.ReadLimit(1))
} else if len(req.Id) > 0 {
recs, err = store.Read(invite.Key(ctx), store.ReadLimit(1), store.ReadPrefix())
} else if len(req.Code) > 0 {
// create a code suffix key
key := ":" + req.Code
// read all the keys with the given code
// TODO: potential race where if the code is not random
// we read it for the wrong user e.g if two tenants generate the same code
r, lerr := store.Read(key, store.ReadLimit(1), store.ReadSuffix())
// now scan for the prefix
prefix := "invite:"
// additional prefix for the tenant
if t, ok := tenant.FromContext(ctx); ok {
prefix = t + "/" + prefix
}
// scan for the key we're looking for
for _, rec := range r {
// skip the missing prefix
if !strings.HasPrefix(rec.Key, prefix) {
continue
}
// skip missing suffix
if !strings.HasSuffix(rec.Key, key) {
continue
}
// save the record
recs = append(recs, rec)
break
}
// set the error
// TODO: maybe just process this
err = lerr
}
// check if there are any records
if err == store.ErrNotFound || len(recs) == 0 {
return ErrInviteNotFound
}
// check the error
if err != nil {
logger.Errorf("Error reading from the store: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// unmarshal the invite
invite.Unmarshal(recs[0].Value)
// serialize the response
rsp.Invite = invite.Serialize()
return nil
}
// List invited for a group or specific email
func (i *Invites) List(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.Email) == 0 && len(req.GroupId) == 0 {
return ErrMissingGroupIDAndEmail
}
invite := &Invite{
GroupID: req.GroupId,
Email: req.Email,
}
var recs []*store.Record
var err error
if len(invite.GroupID) > 0 && len(invite.Email) > 0 {
key := invite.Index(ctx)
recs, err = store.Read(key, store.ReadLimit(1))
} else if len(invite.GroupID) > 0 {
key := invite.Index(ctx)
recs, err = store.Read(key, store.ReadPrefix())
} else if len(invite.Email) > 0 {
// create a email suffix key
key := ":" + invite.Email
// read all the keys with the given code
r, lerr := store.Read(key, store.ReadSuffix())
// now scan for the prefix
prefix := "group:"
// additional prefix for the tenant
if t, ok := tenant.FromContext(ctx); ok {
prefix = t + "/" + prefix
}
// scan for the key we're looking for
for _, rec := range r {
// skip the missing prefix
if !strings.HasPrefix(rec.Key, prefix) {
continue
}
// skip missing suffix
if !strings.HasSuffix(rec.Key, key) {
continue
}
// save the record
recs = append(recs, rec)
}
// set the error
// TODO: maybe just process this
err = lerr
}
// no records found
if err == store.ErrNotFound {
return nil
}
// check the error
if err != nil {
logger.Errorf("Error reading from the store: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// return response
for _, rec := range recs {
invite := new(Invite)
invite.Unmarshal(rec.Value)
rsp.Invites = append(rsp.Invites, invite.Serialize())
}
return nil
}
// Delete an invite
func (i *Invites) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.DeleteResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.Id) == 0 {
return ErrMissingID
}
invite := &Invite{ID: req.Id}
key := invite.Key(ctx)
// check for the existing invite value
recs, err := store.Read(key, store.ReadLimit(1), store.ReadPrefix())
if err == store.ErrNotFound || len(recs) == 0 {
return nil
} else if err != nil {
logger.Errorf("Error connecting to DB: %v", err)
return errors.InternalServerError("DB_ERROR", "Error connecting to DB")
}
// unmarshal the existing invite
invite.Unmarshal(recs[0].Value)
if invite.ID != req.Id {
return nil
}
// delete the record by id
store.Delete(invite.Key(ctx))
// delete the record by group id
store.Delete(invite.Index(ctx))
return nil
}
// isEmailValid checks if the email provided passes the required structure and length.
func isEmailValid(e string) bool {
if len(e) < 3 && len(e) > 254 {
return false
}
return emailRegex.MatchString(e)
}
// generateCode generates a random 8 digit code
func generateCode() string {
v := rand.Intn(89999999) + 10000000
return strconv.Itoa(v)
}

View File

@@ -1,258 +0,0 @@
package handler_test
import (
"context"
"testing"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/store"
"github.com/micro/micro/v3/service/store/memory"
"github.com/micro/services/invites/handler"
pb "github.com/micro/services/invites/proto"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func testHandler(t *testing.T) *handler.Invites {
store.DefaultStore = memory.NewStore()
return &handler.Invites{}
}
func TestCreate(t *testing.T) {
tt := []struct {
Name string
GroupID string
Email string
Error error
}{
{
Name: "MissingGroupID",
Email: "john@doe.com",
Error: handler.ErrMissingGroupID,
},
{
Name: "MissingEmail",
GroupID: uuid.New().String(),
Error: handler.ErrMissingEmail,
},
{
Name: "InvalidEmail",
GroupID: uuid.New().String(),
Email: "foo.foo.foo",
Error: handler.ErrInvalidEmail,
},
{
Name: "Valid",
GroupID: "thisisavalidgroupid",
Email: "john@doe.com",
},
{
Name: "Repeat",
GroupID: "thisisavalidgroupid",
Email: "john@doe.com",
},
}
h := testHandler(t)
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
var rsp pb.CreateResponse
err := h.Create(microAccountCtx(), &pb.CreateRequest{
GroupId: tc.GroupID, Email: tc.Email,
}, &rsp)
assert.Equal(t, tc.Error, err)
if tc.Error != nil {
assert.Nil(t, rsp.Invite)
return
}
if rsp.Invite == nil {
t.Fatalf("Invite was not returned")
return
}
assert.NotEmpty(t, rsp.Invite.Id)
assert.NotEmpty(t, rsp.Invite.Code)
assert.Equal(t, tc.GroupID, rsp.Invite.GroupId)
assert.Equal(t, tc.Email, rsp.Invite.Email)
})
}
}
func TestRead(t *testing.T) {
h := testHandler(t)
// seed some data
var cRsp pb.CreateResponse
err := h.Create(microAccountCtx(), &pb.CreateRequest{Email: "john@doe.com", GroupId: uuid.New().String()}, &cRsp)
assert.NoError(t, err)
if cRsp.Invite == nil {
t.Fatal("No invite returned on create")
return
}
tt := []struct {
Name string
ID string
Code string
Error error
Invite *pb.Invite
}{
{
Name: "MissingIDAndCode",
Error: handler.ErrMissingIDAndCode,
},
{
Name: "NotFoundByID",
ID: uuid.New().String(),
Error: handler.ErrInviteNotFound,
},
{
Name: "NotFoundByCode",
Code: "12345678",
Error: handler.ErrInviteNotFound,
},
{
Name: "ValidID",
ID: cRsp.Invite.Id,
Invite: cRsp.Invite,
},
{
Name: "ValidCode",
Code: cRsp.Invite.Code,
Invite: cRsp.Invite,
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
var rsp pb.ReadResponse
err := h.Read(microAccountCtx(), &pb.ReadRequest{Id: tc.ID, Code: tc.Code}, &rsp)
assert.Equal(t, tc.Error, err)
if tc.Invite == nil {
assert.Nil(t, rsp.Invite)
} else {
assertInvitesMatch(t, tc.Invite, rsp.Invite)
}
})
}
}
func TestList(t *testing.T) {
h := testHandler(t)
// seed some data
var cRsp pb.CreateResponse
err := h.Create(microAccountCtx(), &pb.CreateRequest{Email: "john@doe.com", GroupId: uuid.New().String()}, &cRsp)
assert.NoError(t, err)
if cRsp.Invite == nil {
t.Fatal("No invite returned on create")
return
}
tt := []struct {
Name string
GroupID string
Email string
Error error
Invite *pb.Invite
}{
{
Name: "MissingIDAndEmail",
Error: handler.ErrMissingGroupIDAndEmail,
},
{
Name: "NoResultsForEmail",
Email: "foo@bar.com",
},
{
Name: "NoResultsForGroupID",
GroupID: uuid.New().String(),
},
{
Name: "ValidGroupID",
GroupID: cRsp.Invite.GroupId,
Invite: cRsp.Invite,
},
{
Name: "ValidEmail",
Email: cRsp.Invite.Email,
Invite: cRsp.Invite,
},
{
Name: "EmailAndGroupID",
Email: cRsp.Invite.Email,
GroupID: uuid.New().String(),
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
var rsp pb.ListResponse
err := h.List(microAccountCtx(), &pb.ListRequest{Email: tc.Email, GroupId: tc.GroupID}, &rsp)
assert.Equal(t, tc.Error, err)
if tc.Invite == nil {
assert.Empty(t, rsp.Invites)
} else {
if len(rsp.Invites) != 1 {
t.Errorf("Incorrect number of invites returned, expected 1 but got %v", len(rsp.Invites))
return
}
assertInvitesMatch(t, tc.Invite, rsp.Invites[0])
}
})
}
}
func TestDelete(t *testing.T) {
h := testHandler(t)
t.Run("MissingID", func(t *testing.T) {
err := h.Delete(microAccountCtx(), &pb.DeleteRequest{}, &pb.DeleteResponse{})
assert.Equal(t, handler.ErrMissingID, err)
})
// seed some data
var cRsp pb.CreateResponse
err := h.Create(microAccountCtx(), &pb.CreateRequest{Email: "john@doe.com", GroupId: uuid.New().String()}, &cRsp)
assert.NoError(t, err)
if cRsp.Invite == nil {
t.Fatal("No invite returned on create")
return
}
t.Run("Valid", func(t *testing.T) {
err := h.Delete(microAccountCtx(), &pb.DeleteRequest{Id: cRsp.Invite.Id}, &pb.DeleteResponse{})
assert.NoError(t, err)
err = h.Read(microAccountCtx(), &pb.ReadRequest{Id: cRsp.Invite.Id}, &pb.ReadResponse{})
assert.Equal(t, handler.ErrInviteNotFound, err)
})
t.Run("Repeat", func(t *testing.T) {
err := h.Delete(microAccountCtx(), &pb.DeleteRequest{Id: cRsp.Invite.Id}, &pb.DeleteResponse{})
assert.NoError(t, err)
})
}
func assertInvitesMatch(t *testing.T, exp, act *pb.Invite) {
if act == nil {
t.Errorf("No invite returned")
return
}
assert.Equal(t, exp.Id, act.Id)
assert.Equal(t, exp.Code, act.Code)
assert.Equal(t, exp.Email, act.Email)
assert.Equal(t, exp.GroupId, act.GroupId)
}
func microAccountCtx() context.Context {
return auth.ContextWithAccount(context.TODO(), &auth.Account{
Issuer: "micro",
ID: "someID",
})
}

View File

@@ -1,25 +0,0 @@
package main
import (
"github.com/micro/services/invites/handler"
pb "github.com/micro/services/invites/proto"
"github.com/micro/micro/v3/service"
"github.com/micro/micro/v3/service/logger"
)
func main() {
// Create service
srv := service.New(
service.Name("invites"),
service.Version("latest"),
)
// Register handler
pb.RegisterInvitesHandler(srv.Server(), new(handler.Invites))
// Run service
if err := srv.Run(); err != nil {
logger.Fatal(err)
}
}

View File

@@ -1 +0,0 @@
service invites

View File

@@ -1,716 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v3.15.6
// source: proto/invites.proto
package invites
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Invite struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
GroupId string `protobuf:"bytes,2,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"`
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
Code string `protobuf:"bytes,4,opt,name=code,proto3" json:"code,omitempty"`
}
func (x *Invite) Reset() {
*x = Invite{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_invites_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Invite) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Invite) ProtoMessage() {}
func (x *Invite) ProtoReflect() protoreflect.Message {
mi := &file_proto_invites_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Invite.ProtoReflect.Descriptor instead.
func (*Invite) Descriptor() ([]byte, []int) {
return file_proto_invites_proto_rawDescGZIP(), []int{0}
}
func (x *Invite) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *Invite) GetGroupId() string {
if x != nil {
return x.GroupId
}
return ""
}
func (x *Invite) GetEmail() string {
if x != nil {
return x.Email
}
return ""
}
func (x *Invite) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
type CreateRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"`
Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"`
}
func (x *CreateRequest) Reset() {
*x = CreateRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_invites_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreateRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateRequest) ProtoMessage() {}
func (x *CreateRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_invites_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateRequest.ProtoReflect.Descriptor instead.
func (*CreateRequest) Descriptor() ([]byte, []int) {
return file_proto_invites_proto_rawDescGZIP(), []int{1}
}
func (x *CreateRequest) GetGroupId() string {
if x != nil {
return x.GroupId
}
return ""
}
func (x *CreateRequest) GetEmail() string {
if x != nil {
return x.Email
}
return ""
}
type CreateResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Invite *Invite `protobuf:"bytes,1,opt,name=invite,proto3" json:"invite,omitempty"`
}
func (x *CreateResponse) Reset() {
*x = CreateResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_invites_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreateResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateResponse) ProtoMessage() {}
func (x *CreateResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_invites_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateResponse.ProtoReflect.Descriptor instead.
func (*CreateResponse) Descriptor() ([]byte, []int) {
return file_proto_invites_proto_rawDescGZIP(), []int{2}
}
func (x *CreateResponse) GetInvite() *Invite {
if x != nil {
return x.Invite
}
return nil
}
type ReadRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Code string `protobuf:"bytes,2,opt,name=code,proto3" json:"code,omitempty"`
}
func (x *ReadRequest) Reset() {
*x = ReadRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_invites_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ReadRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ReadRequest) ProtoMessage() {}
func (x *ReadRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_invites_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ReadRequest.ProtoReflect.Descriptor instead.
func (*ReadRequest) Descriptor() ([]byte, []int) {
return file_proto_invites_proto_rawDescGZIP(), []int{3}
}
func (x *ReadRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *ReadRequest) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
type ReadResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Invite *Invite `protobuf:"bytes,1,opt,name=invite,proto3" json:"invite,omitempty"`
}
func (x *ReadResponse) Reset() {
*x = ReadResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_invites_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ReadResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ReadResponse) ProtoMessage() {}
func (x *ReadResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_invites_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ReadResponse.ProtoReflect.Descriptor instead.
func (*ReadResponse) Descriptor() ([]byte, []int) {
return file_proto_invites_proto_rawDescGZIP(), []int{4}
}
func (x *ReadResponse) GetInvite() *Invite {
if x != nil {
return x.Invite
}
return nil
}
type ListRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"`
Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"`
}
func (x *ListRequest) Reset() {
*x = ListRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_invites_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListRequest) ProtoMessage() {}
func (x *ListRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_invites_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListRequest.ProtoReflect.Descriptor instead.
func (*ListRequest) Descriptor() ([]byte, []int) {
return file_proto_invites_proto_rawDescGZIP(), []int{5}
}
func (x *ListRequest) GetGroupId() string {
if x != nil {
return x.GroupId
}
return ""
}
func (x *ListRequest) GetEmail() string {
if x != nil {
return x.Email
}
return ""
}
type ListResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Invites []*Invite `protobuf:"bytes,1,rep,name=invites,proto3" json:"invites,omitempty"`
}
func (x *ListResponse) Reset() {
*x = ListResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_invites_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListResponse) ProtoMessage() {}
func (x *ListResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_invites_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListResponse.ProtoReflect.Descriptor instead.
func (*ListResponse) Descriptor() ([]byte, []int) {
return file_proto_invites_proto_rawDescGZIP(), []int{6}
}
func (x *ListResponse) GetInvites() []*Invite {
if x != nil {
return x.Invites
}
return nil
}
type DeleteRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
}
func (x *DeleteRequest) Reset() {
*x = DeleteRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_invites_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DeleteRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteRequest) ProtoMessage() {}
func (x *DeleteRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_invites_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteRequest.ProtoReflect.Descriptor instead.
func (*DeleteRequest) Descriptor() ([]byte, []int) {
return file_proto_invites_proto_rawDescGZIP(), []int{7}
}
func (x *DeleteRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type DeleteResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *DeleteResponse) Reset() {
*x = DeleteResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_invites_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DeleteResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteResponse) ProtoMessage() {}
func (x *DeleteResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_invites_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteResponse.ProtoReflect.Descriptor instead.
func (*DeleteResponse) Descriptor() ([]byte, []int) {
return file_proto_invites_proto_rawDescGZIP(), []int{8}
}
var File_proto_invites_proto protoreflect.FileDescriptor
var file_proto_invites_proto_rawDesc = []byte{
0x0a, 0x13, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x73, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x73, 0x22, 0x5d,
0x0a, 0x06, 0x49, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75,
0x70, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75,
0x70, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01,
0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64,
0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x40, 0x0a,
0x0d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19,
0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61,
0x69, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22,
0x39, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x27, 0x0a, 0x06, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x76, 0x69,
0x74, 0x65, 0x52, 0x06, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x22, 0x31, 0x0a, 0x0b, 0x52, 0x65,
0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x37, 0x0a,
0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a,
0x06, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e,
0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x52, 0x06,
0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x22, 0x3e, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64,
0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x39, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x07, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65,
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65,
0x73, 0x2e, 0x49, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x52, 0x07, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65,
0x73, 0x22, 0x1f, 0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
0x69, 0x64, 0x22, 0x10, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x32, 0xe9, 0x01, 0x0a, 0x07, 0x49, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x73,
0x12, 0x39, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x69, 0x6e, 0x76,
0x69, 0x74, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x17, 0x2e, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x04, 0x52,
0x65, 0x61, 0x64, 0x12, 0x14, 0x2e, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x52, 0x65,
0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x69, 0x6e, 0x76, 0x69,
0x74, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x33, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x14, 0x2e, 0x69, 0x6e, 0x76, 0x69, 0x74,
0x65, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15,
0x2e, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12,
0x16, 0x2e, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65,
0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x42, 0x11, 0x5a, 0x0f, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x3b, 0x69, 0x6e, 0x76, 0x69,
0x74, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_proto_invites_proto_rawDescOnce sync.Once
file_proto_invites_proto_rawDescData = file_proto_invites_proto_rawDesc
)
func file_proto_invites_proto_rawDescGZIP() []byte {
file_proto_invites_proto_rawDescOnce.Do(func() {
file_proto_invites_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_invites_proto_rawDescData)
})
return file_proto_invites_proto_rawDescData
}
var file_proto_invites_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_proto_invites_proto_goTypes = []interface{}{
(*Invite)(nil), // 0: invites.Invite
(*CreateRequest)(nil), // 1: invites.CreateRequest
(*CreateResponse)(nil), // 2: invites.CreateResponse
(*ReadRequest)(nil), // 3: invites.ReadRequest
(*ReadResponse)(nil), // 4: invites.ReadResponse
(*ListRequest)(nil), // 5: invites.ListRequest
(*ListResponse)(nil), // 6: invites.ListResponse
(*DeleteRequest)(nil), // 7: invites.DeleteRequest
(*DeleteResponse)(nil), // 8: invites.DeleteResponse
}
var file_proto_invites_proto_depIdxs = []int32{
0, // 0: invites.CreateResponse.invite:type_name -> invites.Invite
0, // 1: invites.ReadResponse.invite:type_name -> invites.Invite
0, // 2: invites.ListResponse.invites:type_name -> invites.Invite
1, // 3: invites.Invites.Create:input_type -> invites.CreateRequest
3, // 4: invites.Invites.Read:input_type -> invites.ReadRequest
5, // 5: invites.Invites.List:input_type -> invites.ListRequest
7, // 6: invites.Invites.Delete:input_type -> invites.DeleteRequest
2, // 7: invites.Invites.Create:output_type -> invites.CreateResponse
4, // 8: invites.Invites.Read:output_type -> invites.ReadResponse
6, // 9: invites.Invites.List:output_type -> invites.ListResponse
8, // 10: invites.Invites.Delete:output_type -> invites.DeleteResponse
7, // [7:11] is the sub-list for method output_type
3, // [3:7] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_proto_invites_proto_init() }
func file_proto_invites_proto_init() {
if File_proto_invites_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_proto_invites_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Invite); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_invites_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CreateRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_invites_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CreateResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_invites_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ReadRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_invites_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ReadResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_invites_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_invites_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_invites_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DeleteRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_invites_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DeleteResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_invites_proto_rawDesc,
NumEnums: 0,
NumMessages: 9,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_proto_invites_proto_goTypes,
DependencyIndexes: file_proto_invites_proto_depIdxs,
MessageInfos: file_proto_invites_proto_msgTypes,
}.Build()
File_proto_invites_proto = out.File
file_proto_invites_proto_rawDesc = nil
file_proto_invites_proto_goTypes = nil
file_proto_invites_proto_depIdxs = nil
}

View File

@@ -1,152 +0,0 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: proto/invites.proto
package invites
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
import (
context "context"
api "github.com/micro/micro/v3/service/api"
client "github.com/micro/micro/v3/service/client"
server "github.com/micro/micro/v3/service/server"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
// Reference imports to suppress errors if they are not otherwise used.
var _ api.Endpoint
var _ context.Context
var _ client.Option
var _ server.Option
// Api Endpoints for Invites service
func NewInvitesEndpoints() []*api.Endpoint {
return []*api.Endpoint{}
}
// Client API for Invites service
type InvitesService interface {
// Create an invite
Create(ctx context.Context, in *CreateRequest, opts ...client.CallOption) (*CreateResponse, error)
// Read an invite using ID or code
Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ReadResponse, error)
// List invited for a group or specific email
List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error)
// Delete an invite
Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error)
}
type invitesService struct {
c client.Client
name string
}
func NewInvitesService(name string, c client.Client) InvitesService {
return &invitesService{
c: c,
name: name,
}
}
func (c *invitesService) Create(ctx context.Context, in *CreateRequest, opts ...client.CallOption) (*CreateResponse, error) {
req := c.c.NewRequest(c.name, "Invites.Create", in)
out := new(CreateResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *invitesService) Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ReadResponse, error) {
req := c.c.NewRequest(c.name, "Invites.Read", in)
out := new(ReadResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *invitesService) List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error) {
req := c.c.NewRequest(c.name, "Invites.List", in)
out := new(ListResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *invitesService) Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error) {
req := c.c.NewRequest(c.name, "Invites.Delete", in)
out := new(DeleteResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Invites service
type InvitesHandler interface {
// Create an invite
Create(context.Context, *CreateRequest, *CreateResponse) error
// Read an invite using ID or code
Read(context.Context, *ReadRequest, *ReadResponse) error
// List invited for a group or specific email
List(context.Context, *ListRequest, *ListResponse) error
// Delete an invite
Delete(context.Context, *DeleteRequest, *DeleteResponse) error
}
func RegisterInvitesHandler(s server.Server, hdlr InvitesHandler, opts ...server.HandlerOption) error {
type invites interface {
Create(ctx context.Context, in *CreateRequest, out *CreateResponse) error
Read(ctx context.Context, in *ReadRequest, out *ReadResponse) error
List(ctx context.Context, in *ListRequest, out *ListResponse) error
Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error
}
type Invites struct {
invites
}
h := &invitesHandler{hdlr}
return s.Handle(s.NewHandler(&Invites{h}, opts...))
}
type invitesHandler struct {
InvitesHandler
}
func (h *invitesHandler) Create(ctx context.Context, in *CreateRequest, out *CreateResponse) error {
return h.InvitesHandler.Create(ctx, in, out)
}
func (h *invitesHandler) Read(ctx context.Context, in *ReadRequest, out *ReadResponse) error {
return h.InvitesHandler.Read(ctx, in, out)
}
func (h *invitesHandler) List(ctx context.Context, in *ListRequest, out *ListResponse) error {
return h.InvitesHandler.List(ctx, in, out)
}
func (h *invitesHandler) Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error {
return h.InvitesHandler.Delete(ctx, in, out)
}

View File

@@ -1,55 +0,0 @@
syntax = "proto3";
package invites;
option go_package = "./proto;invites";
service Invites {
// Create an invite
rpc Create(CreateRequest) returns (CreateResponse);
// Read an invite using ID or code
rpc Read(ReadRequest) returns (ReadResponse);
// List invited for a group or specific email
rpc List(ListRequest) returns (ListResponse);
// Delete an invite
rpc Delete(DeleteRequest) returns (DeleteResponse);
}
message Invite {
string id = 1;
string group_id = 2;
string email = 3;
string code = 4;
}
message CreateRequest {
string group_id = 1;
string email = 2;
}
message CreateResponse {
Invite invite = 1;
}
message ReadRequest {
string id = 1;
string code = 2;
}
message ReadResponse {
Invite invite = 1;
}
message ListRequest {
string group_id = 1;
string email = 2;
}
message ListResponse {
repeated Invite invites = 1;
}
message DeleteRequest {
string id = 1;
}
message DeleteResponse {}

View File

@@ -1,27 +0,0 @@
GOPATH:=$(shell go env GOPATH)
.PHONY: init
init:
go get -u github.com/golang/protobuf/proto
go get -u github.com/golang/protobuf/protoc-gen-go
go get github.com/micro/micro/v3/cmd/protoc-gen-micro
.PHONY: proto
proto:
protoc --openapi_out=. --proto_path=. --micro_out=. --go_out=:. proto/mail.proto
.PHONY: docs
docs:
protoc --openapi_out=. --proto_path=. --micro_out=. --go_out=:. proto/mail.proto
@redoc-cli bundle api-mail.json
.PHONY: build
build:
go build -o mail *.go
.PHONY: test
test:
go test -v ./... -cover
.PHONY: docker
docker:
docker build . -t mail:latest

View File

@@ -1,6 +0,0 @@
An inbox for your messages
# Mail Service
The mail service is a simplified service for sending mail, much like email where the messages stay on the server.

View File

@@ -1,116 +0,0 @@
package handler
import (
"context"
"encoding/json"
"strings"
"time"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/store"
pb "github.com/micro/services/mail/proto"
)
const (
messagePrefix = "message"
joinKey = "/"
)
type Mail struct{}
// Send a message
func (m *Mail) Send(ctx context.Context, req *pb.SendRequest, rsp *pb.SendResponse) error {
// validate the request
if len(req.To) == 0 {
return errors.BadRequest("mail.Send.MissingTo", "Missing to")
}
if len(req.From) == 0 {
return errors.BadRequest("mail.Send.MissingFrom", "Missing from")
}
if len(req.Text) == 0 {
return errors.BadRequest("mail.Send.MissingText", "Missing text")
}
// construct the message and marshal it to json
msg := &pb.Message{
Id: uuid.New().String(),
To: req.To,
From: req.From,
Subject: req.Subject,
Text: req.Text,
SentAt: time.Now().Unix(),
}
bytes, err := json.Marshal(msg)
if err != nil {
return errors.BadRequest("mail.Send.Unknown", "Error encoding message")
}
// write the message to the store under the recipients key
key := strings.Join([]string{messagePrefix, req.To, msg.Id}, joinKey)
if err := store.Write(&store.Record{Key: key, Value: bytes}); err != nil {
return errors.BadRequest("mail.Send.Unknown", "Error writing to the store")
}
// write the message to the store under the id (so it can be looked up without needing to know
// the users id)
key = strings.Join([]string{messagePrefix, msg.Id}, joinKey)
if err := store.Write(&store.Record{Key: key, Value: bytes}); err != nil {
return errors.BadRequest("mail.Send.Unknown", "Error writing to the store")
}
return nil
}
// List mail for a user
func (m *Mail) List(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error {
// validate the request
if len(req.User) == 0 {
return errors.BadRequest("mail.List.MissingUser", "Missing user")
}
// query the store for any mail sent to the user
prefix := strings.Join([]string{messagePrefix, req.User}, joinKey)
recs, err := store.Read(prefix, store.ReadPrefix())
if err != nil {
return errors.BadRequest("mail.List.Unknown", "Error reading from the store")
}
// serialize the result
rsp.Mail = make([]*pb.Message, len(recs))
for i, r := range recs {
var msg pb.Message
if err := json.Unmarshal(r.Value, &msg); err != nil {
return errors.BadRequest("mail.List.Unknown", "Error decoding message")
}
rsp.Mail[i] = &msg
}
return nil
}
// Read a message
func (m *Mail) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error {
// validate the request
if len(req.Id) == 0 {
return errors.BadRequest("mail.Read.MissingUser", "Missing user")
}
// query the store
key := strings.Join([]string{messagePrefix, req.Id}, joinKey)
recs, err := store.Read(key)
if err == store.ErrNotFound {
return errors.NotFound("message.Read.InvalidID", "Message not found with ID")
} else if err != nil {
return errors.BadRequest("mail.Read.Unknown", "Error reading from the store")
}
// serialize the response
var msg pb.Message
if err := json.Unmarshal(recs[0].Value, &msg); err != nil {
return errors.BadRequest("mail.Read.Unknown", "Error decoding message")
}
rsp.Message = &msg
return nil
}

View File

@@ -1,25 +0,0 @@
package main
import (
"github.com/micro/micro/v3/service"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/services/mail/handler"
pb "github.com/micro/services/mail/proto"
)
func main() {
// Create the service
srv := service.New(
service.Name("mail"),
service.Version("latest"),
)
// Register the handler against the server
pb.RegisterMailHandler(srv.Server(), new(handler.Mail))
// Run the service
if err := srv.Run(); err != nil {
logger.Fatal(err)
}
}

View File

@@ -1 +0,0 @@
service mail

View File

@@ -1,603 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.25.0
// protoc v3.15.5
// source: proto/mail.proto
package mail
import (
proto "github.com/golang/protobuf/proto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
type Message struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
To string `protobuf:"bytes,2,opt,name=to,proto3" json:"to,omitempty"`
From string `protobuf:"bytes,3,opt,name=from,proto3" json:"from,omitempty"`
Subject string `protobuf:"bytes,4,opt,name=subject,proto3" json:"subject,omitempty"`
Text string `protobuf:"bytes,5,opt,name=text,proto3" json:"text,omitempty"`
SentAt int64 `protobuf:"varint,6,opt,name=sent_at,json=sentAt,proto3" json:"sent_at,omitempty"`
}
func (x *Message) Reset() {
*x = Message{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_mail_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Message) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Message) ProtoMessage() {}
func (x *Message) ProtoReflect() protoreflect.Message {
mi := &file_proto_mail_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Message.ProtoReflect.Descriptor instead.
func (*Message) Descriptor() ([]byte, []int) {
return file_proto_mail_proto_rawDescGZIP(), []int{0}
}
func (x *Message) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *Message) GetTo() string {
if x != nil {
return x.To
}
return ""
}
func (x *Message) GetFrom() string {
if x != nil {
return x.From
}
return ""
}
func (x *Message) GetSubject() string {
if x != nil {
return x.Subject
}
return ""
}
func (x *Message) GetText() string {
if x != nil {
return x.Text
}
return ""
}
func (x *Message) GetSentAt() int64 {
if x != nil {
return x.SentAt
}
return 0
}
type SendRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
To string `protobuf:"bytes,1,opt,name=to,proto3" json:"to,omitempty"`
From string `protobuf:"bytes,2,opt,name=from,proto3" json:"from,omitempty"`
Subject string `protobuf:"bytes,3,opt,name=subject,proto3" json:"subject,omitempty"`
Text string `protobuf:"bytes,4,opt,name=text,proto3" json:"text,omitempty"`
}
func (x *SendRequest) Reset() {
*x = SendRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_mail_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SendRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SendRequest) ProtoMessage() {}
func (x *SendRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mail_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SendRequest.ProtoReflect.Descriptor instead.
func (*SendRequest) Descriptor() ([]byte, []int) {
return file_proto_mail_proto_rawDescGZIP(), []int{1}
}
func (x *SendRequest) GetTo() string {
if x != nil {
return x.To
}
return ""
}
func (x *SendRequest) GetFrom() string {
if x != nil {
return x.From
}
return ""
}
func (x *SendRequest) GetSubject() string {
if x != nil {
return x.Subject
}
return ""
}
func (x *SendRequest) GetText() string {
if x != nil {
return x.Text
}
return ""
}
type SendResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *SendResponse) Reset() {
*x = SendResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_mail_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SendResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SendResponse) ProtoMessage() {}
func (x *SendResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mail_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SendResponse.ProtoReflect.Descriptor instead.
func (*SendResponse) Descriptor() ([]byte, []int) {
return file_proto_mail_proto_rawDescGZIP(), []int{2}
}
type ListRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
}
func (x *ListRequest) Reset() {
*x = ListRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_mail_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListRequest) ProtoMessage() {}
func (x *ListRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mail_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListRequest.ProtoReflect.Descriptor instead.
func (*ListRequest) Descriptor() ([]byte, []int) {
return file_proto_mail_proto_rawDescGZIP(), []int{3}
}
func (x *ListRequest) GetUser() string {
if x != nil {
return x.User
}
return ""
}
type ListResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Mail []*Message `protobuf:"bytes,1,rep,name=mail,proto3" json:"mail,omitempty"`
}
func (x *ListResponse) Reset() {
*x = ListResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_mail_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListResponse) ProtoMessage() {}
func (x *ListResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mail_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListResponse.ProtoReflect.Descriptor instead.
func (*ListResponse) Descriptor() ([]byte, []int) {
return file_proto_mail_proto_rawDescGZIP(), []int{4}
}
func (x *ListResponse) GetMail() []*Message {
if x != nil {
return x.Mail
}
return nil
}
type ReadRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
}
func (x *ReadRequest) Reset() {
*x = ReadRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_mail_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ReadRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ReadRequest) ProtoMessage() {}
func (x *ReadRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mail_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ReadRequest.ProtoReflect.Descriptor instead.
func (*ReadRequest) Descriptor() ([]byte, []int) {
return file_proto_mail_proto_rawDescGZIP(), []int{5}
}
func (x *ReadRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type ReadResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message *Message `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
func (x *ReadResponse) Reset() {
*x = ReadResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_mail_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ReadResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ReadResponse) ProtoMessage() {}
func (x *ReadResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mail_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ReadResponse.ProtoReflect.Descriptor instead.
func (*ReadResponse) Descriptor() ([]byte, []int) {
return file_proto_mail_proto_rawDescGZIP(), []int{6}
}
func (x *ReadResponse) GetMessage() *Message {
if x != nil {
return x.Message
}
return nil
}
var File_proto_mail_proto protoreflect.FileDescriptor
var file_proto_mail_proto_rawDesc = []byte{
0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x12, 0x04, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x84, 0x01, 0x0a, 0x07, 0x4d, 0x65, 0x73,
0x73, 0x61, 0x67, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x02, 0x69, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x02, 0x74, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x03, 0x20, 0x01,
0x28, 0x09, 0x52, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a,
0x65, 0x63, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65,
0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x65, 0x6e, 0x74, 0x5f, 0x61,
0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x73, 0x65, 0x6e, 0x74, 0x41, 0x74, 0x22,
0x5f, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e,
0x0a, 0x02, 0x74, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x74, 0x6f, 0x12, 0x12,
0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x72,
0x6f, 0x6d, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04,
0x74, 0x65, 0x78, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74,
0x22, 0x0e, 0x0a, 0x0c, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x21, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75,
0x73, 0x65, 0x72, 0x22, 0x31, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x04, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28,
0x0b, 0x32, 0x0d, 0x2e, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x52, 0x04, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x1d, 0x0a, 0x0b, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x37, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x4d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x93,
0x01, 0x0a, 0x04, 0x4d, 0x61, 0x69, 0x6c, 0x12, 0x2d, 0x0a, 0x04, 0x53, 0x65, 0x6e, 0x64, 0x12,
0x11, 0x2e, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x12, 0x2e, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x11,
0x2e, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x12, 0x2e, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x11, 0x2e,
0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x12, 0x2e, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x42, 0x0c, 0x5a, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x3b, 0x6d, 0x61,
0x69, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_proto_mail_proto_rawDescOnce sync.Once
file_proto_mail_proto_rawDescData = file_proto_mail_proto_rawDesc
)
func file_proto_mail_proto_rawDescGZIP() []byte {
file_proto_mail_proto_rawDescOnce.Do(func() {
file_proto_mail_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_mail_proto_rawDescData)
})
return file_proto_mail_proto_rawDescData
}
var file_proto_mail_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_proto_mail_proto_goTypes = []interface{}{
(*Message)(nil), // 0: mail.Message
(*SendRequest)(nil), // 1: mail.SendRequest
(*SendResponse)(nil), // 2: mail.SendResponse
(*ListRequest)(nil), // 3: mail.ListRequest
(*ListResponse)(nil), // 4: mail.ListResponse
(*ReadRequest)(nil), // 5: mail.ReadRequest
(*ReadResponse)(nil), // 6: mail.ReadResponse
}
var file_proto_mail_proto_depIdxs = []int32{
0, // 0: mail.ListResponse.mail:type_name -> mail.Message
0, // 1: mail.ReadResponse.message:type_name -> mail.Message
1, // 2: mail.Mail.Send:input_type -> mail.SendRequest
3, // 3: mail.Mail.List:input_type -> mail.ListRequest
5, // 4: mail.Mail.Read:input_type -> mail.ReadRequest
2, // 5: mail.Mail.Send:output_type -> mail.SendResponse
4, // 6: mail.Mail.List:output_type -> mail.ListResponse
6, // 7: mail.Mail.Read:output_type -> mail.ReadResponse
5, // [5:8] is the sub-list for method output_type
2, // [2:5] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_proto_mail_proto_init() }
func file_proto_mail_proto_init() {
if File_proto_mail_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_proto_mail_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Message); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_mail_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SendRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_mail_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SendResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_mail_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_mail_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_mail_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ReadRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_mail_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ReadResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_mail_proto_rawDesc,
NumEnums: 0,
NumMessages: 7,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_proto_mail_proto_goTypes,
DependencyIndexes: file_proto_mail_proto_depIdxs,
MessageInfos: file_proto_mail_proto_msgTypes,
}.Build()
File_proto_mail_proto = out.File
file_proto_mail_proto_rawDesc = nil
file_proto_mail_proto_goTypes = nil
file_proto_mail_proto_depIdxs = nil
}

View File

@@ -1,127 +0,0 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: proto/mail.proto
package mail
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
import (
context "context"
api "github.com/micro/micro/v3/service/api"
client "github.com/micro/micro/v3/service/client"
server "github.com/micro/micro/v3/service/server"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
// Reference imports to suppress errors if they are not otherwise used.
var _ api.Endpoint
var _ context.Context
var _ client.Option
var _ server.Option
// Api Endpoints for Mail service
func NewMailEndpoints() []*api.Endpoint {
return []*api.Endpoint{}
}
// Client API for Mail service
type MailService interface {
Send(ctx context.Context, in *SendRequest, opts ...client.CallOption) (*SendResponse, error)
List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error)
Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ReadResponse, error)
}
type mailService struct {
c client.Client
name string
}
func NewMailService(name string, c client.Client) MailService {
return &mailService{
c: c,
name: name,
}
}
func (c *mailService) Send(ctx context.Context, in *SendRequest, opts ...client.CallOption) (*SendResponse, error) {
req := c.c.NewRequest(c.name, "Mail.Send", in)
out := new(SendResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *mailService) List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error) {
req := c.c.NewRequest(c.name, "Mail.List", in)
out := new(ListResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *mailService) Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ReadResponse, error) {
req := c.c.NewRequest(c.name, "Mail.Read", in)
out := new(ReadResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Mail service
type MailHandler interface {
Send(context.Context, *SendRequest, *SendResponse) error
List(context.Context, *ListRequest, *ListResponse) error
Read(context.Context, *ReadRequest, *ReadResponse) error
}
func RegisterMailHandler(s server.Server, hdlr MailHandler, opts ...server.HandlerOption) error {
type mail interface {
Send(ctx context.Context, in *SendRequest, out *SendResponse) error
List(ctx context.Context, in *ListRequest, out *ListResponse) error
Read(ctx context.Context, in *ReadRequest, out *ReadResponse) error
}
type Mail struct {
mail
}
h := &mailHandler{hdlr}
return s.Handle(s.NewHandler(&Mail{h}, opts...))
}
type mailHandler struct {
MailHandler
}
func (h *mailHandler) Send(ctx context.Context, in *SendRequest, out *SendResponse) error {
return h.MailHandler.Send(ctx, in, out)
}
func (h *mailHandler) List(ctx context.Context, in *ListRequest, out *ListResponse) error {
return h.MailHandler.List(ctx, in, out)
}
func (h *mailHandler) Read(ctx context.Context, in *ReadRequest, out *ReadResponse) error {
return h.MailHandler.Read(ctx, in, out)
}

View File

@@ -1,44 +0,0 @@
syntax = "proto3";
package mail;
option go_package = "./proto;mail";
service Mail {
rpc Send(SendRequest) returns (SendResponse);
rpc List(ListRequest) returns (ListResponse);
rpc Read(ReadRequest) returns (ReadResponse);
}
message Message {
string id = 1;
string to = 2;
string from = 3;
string subject = 4;
string text = 5;
int64 sent_at = 6;
}
message SendRequest {
string to = 1;
string from = 2;
string subject = 3;
string text = 4;
}
message SendResponse {}
message ListRequest {
string user = 1;
}
message ListResponse {
repeated Message mail = 1;
}
message ReadRequest {
string id = 1;
}
message ReadResponse {
Message message = 1;
}

View File

View File

@@ -1,49 +0,0 @@
The mail service is a simplified service for sending mail, much like email.
# Mail Service
## Send a message
### CLI
```bash
> micro mail send --to=John --from=Barry --subject=HelloWorld --text="Hello John"
```
## List the mail a user has received
### CLI
```bash
> micro mail list --user=John
{
"mail": [
{
"id": "78efd836-ca51-4163-af43-65985f7c6587",
"to": "John",
"from": "Barry",
"subject": "HelloWorld",
"text": "Hello John",
"sent_at": "1602777240"
}
]
}
```
## Lookup an individual email by ID
### CLI
```bash
> micro mail read --id=78efd836-ca51-4163-af43-65985f7c6587
{
"message": {
"id": "78efd836-ca51-4163-af43-65985f7c6587",
"to": "John",
"from": "Barry",
"subject": "HelloWorld",
"text": "Hello John",
"sent_at": "1602777240"
}
}
```

View File

@@ -1,6 +0,0 @@
Store your notes and todos
# Notes Service
The notes service offers basic storage of notes and todo lists.

View File

@@ -1,161 +0,0 @@
package handler
import (
"context"
"encoding/json"
"io"
"time"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/micro/v3/service/store"
pb "github.com/micro/services/notes/proto"
)
const storePrefix = "notes/"
// New returns an initialized notes handler
func New() pb.NotesHandler {
return new(handler)
}
type handler struct{}
// List all the notes
func (h *handler) List(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error {
// query the store
recs, err := store.Read(storePrefix, store.ReadPrefix())
if err != nil {
logger.Errorf("Error reading notes from the store: %v", err)
return errors.InternalServerError("notes.List.Unknown", "Error reading from the store")
}
// serialize the response
rsp.Notes = make([]*pb.Note, len(recs))
for i, r := range recs {
var note pb.Note
if err := json.Unmarshal(r.Value, &note); err != nil {
logger.Errorf("Error unmarshaling note: %v", err)
return errors.InternalServerError("notes.List.Unknown", "Error unmarshaling note")
}
rsp.Notes[i] = &note
}
return nil
}
// Create a note
func (h *handler) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error {
// validate the request
if len(req.Title) == 0 {
return errors.BadRequest("notes.Create.MissingTitle", "Missing title")
}
// construct the note
note := &pb.Note{
Id: uuid.New().String(),
Created: time.Now().Unix(),
Title: req.Title,
Text: req.Text,
}
// marshal the note to bytes
bytes, err := json.Marshal(note)
if err != nil {
logger.Errorf("Error marshaling note: %v", err)
return errors.InternalServerError("notes.Create.Unknown", "Error marshaling note")
}
// write to the store
key := storePrefix + note.Id
if err := store.Write(&store.Record{Key: key, Value: bytes}); err != nil {
logger.Errorf("Error writing to store: %v", err)
return errors.InternalServerError("notes.Create.Unknown", "Error writing to store")
}
// return the id
rsp.Id = note.Id
return nil
}
// Delete a note
func (h *handler) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.DeleteResponse) error {
// validate the request
if len(req.Id) == 0 {
return errors.BadRequest("notes.Delete.MissingID", "Missing id")
}
// delete the note from the store
if err := store.Delete(storePrefix + req.Id); err == store.ErrNotFound {
return errors.NotFound("notes.Delete.InvalidID", "Note not found with this ID")
} else if err != nil {
logger.Errorf("Error deleting from the store: %v", err)
return errors.InternalServerError("notes.Delete.Unknown", "Error deleting from the store")
}
return nil
}
// Update a note
func (h *handler) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UpdateResponse) error {
// validate the request
if len(req.Id) == 0 {
return errors.BadRequest("notes.Update.MissingID", "Missing id")
}
if len(req.Title) == 0 {
return errors.BadRequest("notes.Update.MissingTitle", "Missing title")
}
// read the note from the store
recs, err := store.Read(storePrefix + req.Id)
if err == store.ErrNotFound {
return errors.NotFound("notes.Update.InvalidID", "Note not found with this ID")
} else if err != nil {
logger.Errorf("Error reading from the store: %v", err)
return errors.InternalServerError("notes.Update.Unknown", "Error reading from the store")
}
// unmarshal the note
var note pb.Note
if err := json.Unmarshal(recs[0].Value, &note); err != nil {
logger.Errorf("Error unmarshaling note: %v", err)
return errors.InternalServerError("notes.Update.Unknown", "Error unmarshaling note")
}
// assign the new title and text
note.Title = req.Title
note.Text = req.Text
// marshal the note to bytes
bytes, err := json.Marshal(note)
if err != nil {
logger.Errorf("Error marshaling note: %v", err)
return errors.InternalServerError("notes.Update.Unknown", "Error marshaling note")
}
// write to the store
key := storePrefix + note.Id
if err := store.Write(&store.Record{Key: key, Value: bytes}); err != nil {
logger.Errorf("Error writing to store: %v", err)
return errors.InternalServerError("notes.Update.Unknown", "Error writing to store")
}
return nil
}
// UpdateStream updates a note every time an update is sent on the stream
func (h *handler) UpdateStream(ctx context.Context, stream pb.Notes_UpdateStreamStream) error {
for {
uReq, err := stream.Recv()
if err == io.EOF {
return nil
} else if err != nil {
return errors.InternalServerError("notes.UpdateStream.Unknown", "Error reading from stream")
}
if err := h.Update(ctx, uReq, nil); err != nil {
return err
}
}
}

View File

@@ -1,22 +0,0 @@
package main
import (
"github.com/micro/micro/v3/service"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/services/notes/handler"
pb "github.com/micro/services/notes/proto"
)
func main() {
srv := service.New(
service.Name("notes"),
service.Version("latest"),
)
pb.RegisterNotesHandler(srv.Server(), handler.New())
if err := srv.Run(); err != nil {
logger.Fatal(err)
}
}

View File

@@ -1 +0,0 @@
service notes

View File

@@ -1,436 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: notes/proto/notes.proto
package notes
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
type Note struct {
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Created int64 `protobuf:"varint,2,opt,name=created,proto3" json:"created,omitempty"`
Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"`
Text string `protobuf:"bytes,4,opt,name=text,proto3" json:"text,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Note) Reset() { *m = Note{} }
func (m *Note) String() string { return proto.CompactTextString(m) }
func (*Note) ProtoMessage() {}
func (*Note) Descriptor() ([]byte, []int) {
return fileDescriptor_7f19915b807268a5, []int{0}
}
func (m *Note) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Note.Unmarshal(m, b)
}
func (m *Note) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Note.Marshal(b, m, deterministic)
}
func (m *Note) XXX_Merge(src proto.Message) {
xxx_messageInfo_Note.Merge(m, src)
}
func (m *Note) XXX_Size() int {
return xxx_messageInfo_Note.Size(m)
}
func (m *Note) XXX_DiscardUnknown() {
xxx_messageInfo_Note.DiscardUnknown(m)
}
var xxx_messageInfo_Note proto.InternalMessageInfo
func (m *Note) GetId() string {
if m != nil {
return m.Id
}
return ""
}
func (m *Note) GetCreated() int64 {
if m != nil {
return m.Created
}
return 0
}
func (m *Note) GetTitle() string {
if m != nil {
return m.Title
}
return ""
}
func (m *Note) GetText() string {
if m != nil {
return m.Text
}
return ""
}
type CreateRequest struct {
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
Text string `protobuf:"bytes,2,opt,name=text,proto3" json:"text,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *CreateRequest) Reset() { *m = CreateRequest{} }
func (m *CreateRequest) String() string { return proto.CompactTextString(m) }
func (*CreateRequest) ProtoMessage() {}
func (*CreateRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_7f19915b807268a5, []int{1}
}
func (m *CreateRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_CreateRequest.Unmarshal(m, b)
}
func (m *CreateRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_CreateRequest.Marshal(b, m, deterministic)
}
func (m *CreateRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_CreateRequest.Merge(m, src)
}
func (m *CreateRequest) XXX_Size() int {
return xxx_messageInfo_CreateRequest.Size(m)
}
func (m *CreateRequest) XXX_DiscardUnknown() {
xxx_messageInfo_CreateRequest.DiscardUnknown(m)
}
var xxx_messageInfo_CreateRequest proto.InternalMessageInfo
func (m *CreateRequest) GetTitle() string {
if m != nil {
return m.Title
}
return ""
}
func (m *CreateRequest) GetText() string {
if m != nil {
return m.Text
}
return ""
}
type CreateResponse struct {
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *CreateResponse) Reset() { *m = CreateResponse{} }
func (m *CreateResponse) String() string { return proto.CompactTextString(m) }
func (*CreateResponse) ProtoMessage() {}
func (*CreateResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_7f19915b807268a5, []int{2}
}
func (m *CreateResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_CreateResponse.Unmarshal(m, b)
}
func (m *CreateResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_CreateResponse.Marshal(b, m, deterministic)
}
func (m *CreateResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_CreateResponse.Merge(m, src)
}
func (m *CreateResponse) XXX_Size() int {
return xxx_messageInfo_CreateResponse.Size(m)
}
func (m *CreateResponse) XXX_DiscardUnknown() {
xxx_messageInfo_CreateResponse.DiscardUnknown(m)
}
var xxx_messageInfo_CreateResponse proto.InternalMessageInfo
func (m *CreateResponse) GetId() string {
if m != nil {
return m.Id
}
return ""
}
type UpdateRequest struct {
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
Text string `protobuf:"bytes,3,opt,name=text,proto3" json:"text,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *UpdateRequest) Reset() { *m = UpdateRequest{} }
func (m *UpdateRequest) String() string { return proto.CompactTextString(m) }
func (*UpdateRequest) ProtoMessage() {}
func (*UpdateRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_7f19915b807268a5, []int{3}
}
func (m *UpdateRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_UpdateRequest.Unmarshal(m, b)
}
func (m *UpdateRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_UpdateRequest.Marshal(b, m, deterministic)
}
func (m *UpdateRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_UpdateRequest.Merge(m, src)
}
func (m *UpdateRequest) XXX_Size() int {
return xxx_messageInfo_UpdateRequest.Size(m)
}
func (m *UpdateRequest) XXX_DiscardUnknown() {
xxx_messageInfo_UpdateRequest.DiscardUnknown(m)
}
var xxx_messageInfo_UpdateRequest proto.InternalMessageInfo
func (m *UpdateRequest) GetId() string {
if m != nil {
return m.Id
}
return ""
}
func (m *UpdateRequest) GetTitle() string {
if m != nil {
return m.Title
}
return ""
}
func (m *UpdateRequest) GetText() string {
if m != nil {
return m.Text
}
return ""
}
type UpdateResponse struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *UpdateResponse) Reset() { *m = UpdateResponse{} }
func (m *UpdateResponse) String() string { return proto.CompactTextString(m) }
func (*UpdateResponse) ProtoMessage() {}
func (*UpdateResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_7f19915b807268a5, []int{4}
}
func (m *UpdateResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_UpdateResponse.Unmarshal(m, b)
}
func (m *UpdateResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_UpdateResponse.Marshal(b, m, deterministic)
}
func (m *UpdateResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_UpdateResponse.Merge(m, src)
}
func (m *UpdateResponse) XXX_Size() int {
return xxx_messageInfo_UpdateResponse.Size(m)
}
func (m *UpdateResponse) XXX_DiscardUnknown() {
xxx_messageInfo_UpdateResponse.DiscardUnknown(m)
}
var xxx_messageInfo_UpdateResponse proto.InternalMessageInfo
type DeleteRequest struct {
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *DeleteRequest) Reset() { *m = DeleteRequest{} }
func (m *DeleteRequest) String() string { return proto.CompactTextString(m) }
func (*DeleteRequest) ProtoMessage() {}
func (*DeleteRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_7f19915b807268a5, []int{5}
}
func (m *DeleteRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_DeleteRequest.Unmarshal(m, b)
}
func (m *DeleteRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_DeleteRequest.Marshal(b, m, deterministic)
}
func (m *DeleteRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_DeleteRequest.Merge(m, src)
}
func (m *DeleteRequest) XXX_Size() int {
return xxx_messageInfo_DeleteRequest.Size(m)
}
func (m *DeleteRequest) XXX_DiscardUnknown() {
xxx_messageInfo_DeleteRequest.DiscardUnknown(m)
}
var xxx_messageInfo_DeleteRequest proto.InternalMessageInfo
func (m *DeleteRequest) GetId() string {
if m != nil {
return m.Id
}
return ""
}
type DeleteResponse struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *DeleteResponse) Reset() { *m = DeleteResponse{} }
func (m *DeleteResponse) String() string { return proto.CompactTextString(m) }
func (*DeleteResponse) ProtoMessage() {}
func (*DeleteResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_7f19915b807268a5, []int{6}
}
func (m *DeleteResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_DeleteResponse.Unmarshal(m, b)
}
func (m *DeleteResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_DeleteResponse.Marshal(b, m, deterministic)
}
func (m *DeleteResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_DeleteResponse.Merge(m, src)
}
func (m *DeleteResponse) XXX_Size() int {
return xxx_messageInfo_DeleteResponse.Size(m)
}
func (m *DeleteResponse) XXX_DiscardUnknown() {
xxx_messageInfo_DeleteResponse.DiscardUnknown(m)
}
var xxx_messageInfo_DeleteResponse proto.InternalMessageInfo
type ListRequest struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ListRequest) Reset() { *m = ListRequest{} }
func (m *ListRequest) String() string { return proto.CompactTextString(m) }
func (*ListRequest) ProtoMessage() {}
func (*ListRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_7f19915b807268a5, []int{7}
}
func (m *ListRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ListRequest.Unmarshal(m, b)
}
func (m *ListRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ListRequest.Marshal(b, m, deterministic)
}
func (m *ListRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_ListRequest.Merge(m, src)
}
func (m *ListRequest) XXX_Size() int {
return xxx_messageInfo_ListRequest.Size(m)
}
func (m *ListRequest) XXX_DiscardUnknown() {
xxx_messageInfo_ListRequest.DiscardUnknown(m)
}
var xxx_messageInfo_ListRequest proto.InternalMessageInfo
type ListResponse struct {
Notes []*Note `protobuf:"bytes,1,rep,name=notes,proto3" json:"notes,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ListResponse) Reset() { *m = ListResponse{} }
func (m *ListResponse) String() string { return proto.CompactTextString(m) }
func (*ListResponse) ProtoMessage() {}
func (*ListResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_7f19915b807268a5, []int{8}
}
func (m *ListResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ListResponse.Unmarshal(m, b)
}
func (m *ListResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ListResponse.Marshal(b, m, deterministic)
}
func (m *ListResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_ListResponse.Merge(m, src)
}
func (m *ListResponse) XXX_Size() int {
return xxx_messageInfo_ListResponse.Size(m)
}
func (m *ListResponse) XXX_DiscardUnknown() {
xxx_messageInfo_ListResponse.DiscardUnknown(m)
}
var xxx_messageInfo_ListResponse proto.InternalMessageInfo
func (m *ListResponse) GetNotes() []*Note {
if m != nil {
return m.Notes
}
return nil
}
func init() {
proto.RegisterType((*Note)(nil), "notes.Note")
proto.RegisterType((*CreateRequest)(nil), "notes.CreateRequest")
proto.RegisterType((*CreateResponse)(nil), "notes.CreateResponse")
proto.RegisterType((*UpdateRequest)(nil), "notes.UpdateRequest")
proto.RegisterType((*UpdateResponse)(nil), "notes.UpdateResponse")
proto.RegisterType((*DeleteRequest)(nil), "notes.DeleteRequest")
proto.RegisterType((*DeleteResponse)(nil), "notes.DeleteResponse")
proto.RegisterType((*ListRequest)(nil), "notes.ListRequest")
proto.RegisterType((*ListResponse)(nil), "notes.ListResponse")
}
func init() { proto.RegisterFile("notes/proto/notes.proto", fileDescriptor_7f19915b807268a5) }
var fileDescriptor_7f19915b807268a5 = []byte{
// 344 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x92, 0xcf, 0x4a, 0xc3, 0x40,
0x10, 0xc6, 0xc9, 0x9f, 0x56, 0x9c, 0x36, 0x45, 0xd6, 0x8a, 0xa1, 0x17, 0x63, 0x4e, 0x01, 0xb1,
0xc1, 0x8a, 0x07, 0x11, 0x2f, 0xea, 0x45, 0x10, 0x0f, 0x11, 0x2f, 0xbd, 0xb5, 0xc9, 0xa0, 0x0b,
0x6d, 0x37, 0x66, 0xa7, 0xe2, 0xc3, 0xf8, 0xb0, 0x92, 0xdd, 0x24, 0xcd, 0xd6, 0x7a, 0xf0, 0x36,
0xdf, 0xec, 0xfc, 0x66, 0x76, 0xbe, 0x5d, 0x38, 0x5e, 0x09, 0x42, 0x19, 0xe7, 0x85, 0x20, 0x11,
0xab, 0x78, 0xac, 0x62, 0xd6, 0x51, 0x22, 0x9c, 0x82, 0xfb, 0x2c, 0x08, 0xd9, 0x00, 0x6c, 0x9e,
0xf9, 0x56, 0x60, 0x45, 0xfb, 0x89, 0xcd, 0x33, 0xe6, 0xc3, 0x5e, 0x5a, 0xe0, 0x8c, 0x30, 0xf3,
0xed, 0xc0, 0x8a, 0x9c, 0xa4, 0x96, 0x6c, 0x08, 0x1d, 0xe2, 0xb4, 0x40, 0xdf, 0x51, 0xc5, 0x5a,
0x30, 0x06, 0x2e, 0xe1, 0x17, 0xf9, 0xae, 0x4a, 0xaa, 0x38, 0xbc, 0x06, 0xef, 0x5e, 0x41, 0x09,
0x7e, 0xac, 0x51, 0xd2, 0x06, 0xb5, 0x76, 0xa1, 0x76, 0x0b, 0x0d, 0x60, 0x50, 0xa3, 0x32, 0x17,
0x2b, 0xf9, 0xeb, 0x82, 0xe1, 0x23, 0x78, 0xaf, 0x79, 0xd6, 0x6a, 0xbe, 0xbd, 0x41, 0x33, 0xcc,
0xde, 0x35, 0xcc, 0x69, 0x0d, 0x3b, 0x80, 0x41, 0xdd, 0x4a, 0x0f, 0x0b, 0x4f, 0xc0, 0x7b, 0xc0,
0x05, 0xfe, 0xd9, 0xbc, 0x44, 0xea, 0x82, 0x0a, 0xf1, 0xa0, 0xf7, 0xc4, 0x25, 0x55, 0x40, 0x78,
0x01, 0x7d, 0x2d, 0xab, 0xeb, 0x9f, 0x82, 0x36, 0xdc, 0xb7, 0x02, 0x27, 0xea, 0x4d, 0x7a, 0x63,
0xfd, 0x16, 0xa5, 0xf7, 0x89, 0x3e, 0x99, 0x7c, 0xdb, 0xd0, 0x29, 0xb5, 0x64, 0x31, 0xb8, 0x25,
0xcc, 0x58, 0x55, 0xd5, 0x6a, 0x3c, 0x3a, 0x34, 0x72, 0x55, 0xf7, 0x2b, 0xe8, 0x6a, 0xbb, 0xd8,
0xb0, 0x3a, 0x36, 0x8c, 0x1f, 0x1d, 0x6d, 0x65, 0x37, 0x98, 0xde, 0xa2, 0xc1, 0x8c, 0xad, 0x1b,
0xcc, 0x5c, 0xb5, 0xc4, 0xb4, 0x5f, 0x0d, 0x66, 0xbc, 0x44, 0x83, 0x99, 0xa6, 0xb2, 0x5b, 0xe8,
0xeb, 0xcc, 0x0b, 0x15, 0x38, 0x5b, 0xfe, 0x0b, 0x8e, 0xac, 0xbb, 0xf3, 0xe9, 0xd9, 0x1b, 0xa7,
0xf7, 0xf5, 0x7c, 0x9c, 0x8a, 0x65, 0xbc, 0xe4, 0x69, 0x21, 0x62, 0x89, 0xc5, 0x27, 0x4f, 0x51,
0xc6, 0xad, 0x5f, 0x7e, 0xa3, 0xe2, 0x79, 0x57, 0x89, 0xcb, 0x9f, 0x00, 0x00, 0x00, 0xff, 0xff,
0x31, 0xee, 0xed, 0xda, 0x01, 0x03, 0x00, 0x00,
}

View File

@@ -1,236 +0,0 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: notes/proto/notes.proto
package notes
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
import (
context "context"
api "github.com/micro/micro/v3/service/api"
client "github.com/micro/micro/v3/service/client"
server "github.com/micro/micro/v3/service/server"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
// Reference imports to suppress errors if they are not otherwise used.
var _ api.Endpoint
var _ context.Context
var _ client.Option
var _ server.Option
// Api Endpoints for Notes service
func NewNotesEndpoints() []*api.Endpoint {
return []*api.Endpoint{}
}
// Client API for Notes service
type NotesService interface {
List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error)
Create(ctx context.Context, in *CreateRequest, opts ...client.CallOption) (*CreateResponse, error)
Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error)
Update(ctx context.Context, in *UpdateRequest, opts ...client.CallOption) (*UpdateResponse, error)
UpdateStream(ctx context.Context, opts ...client.CallOption) (Notes_UpdateStreamService, error)
}
type notesService struct {
c client.Client
name string
}
func NewNotesService(name string, c client.Client) NotesService {
return &notesService{
c: c,
name: name,
}
}
func (c *notesService) List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error) {
req := c.c.NewRequest(c.name, "Notes.List", in)
out := new(ListResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notesService) Create(ctx context.Context, in *CreateRequest, opts ...client.CallOption) (*CreateResponse, error) {
req := c.c.NewRequest(c.name, "Notes.Create", in)
out := new(CreateResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notesService) Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error) {
req := c.c.NewRequest(c.name, "Notes.Delete", in)
out := new(DeleteResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notesService) Update(ctx context.Context, in *UpdateRequest, opts ...client.CallOption) (*UpdateResponse, error) {
req := c.c.NewRequest(c.name, "Notes.Update", in)
out := new(UpdateResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notesService) UpdateStream(ctx context.Context, opts ...client.CallOption) (Notes_UpdateStreamService, error) {
req := c.c.NewRequest(c.name, "Notes.UpdateStream", &UpdateRequest{})
stream, err := c.c.Stream(ctx, req, opts...)
if err != nil {
return nil, err
}
return &notesServiceUpdateStream{stream}, nil
}
type Notes_UpdateStreamService interface {
Context() context.Context
SendMsg(interface{}) error
RecvMsg(interface{}) error
CloseAndRecv() (*UpdateResponse, error)
Send(*UpdateRequest) error
}
type notesServiceUpdateStream struct {
stream client.Stream
}
func (x *notesServiceUpdateStream) CloseAndRecv() (*UpdateResponse, error) {
if err := x.stream.Close(); err != nil {
return nil, err
}
r := new(UpdateResponse)
err := x.RecvMsg(r)
return r, err
}
func (x *notesServiceUpdateStream) Context() context.Context {
return x.stream.Context()
}
func (x *notesServiceUpdateStream) SendMsg(m interface{}) error {
return x.stream.Send(m)
}
func (x *notesServiceUpdateStream) RecvMsg(m interface{}) error {
return x.stream.Recv(m)
}
func (x *notesServiceUpdateStream) Send(m *UpdateRequest) error {
return x.stream.Send(m)
}
// Server API for Notes service
type NotesHandler interface {
List(context.Context, *ListRequest, *ListResponse) error
Create(context.Context, *CreateRequest, *CreateResponse) error
Delete(context.Context, *DeleteRequest, *DeleteResponse) error
Update(context.Context, *UpdateRequest, *UpdateResponse) error
UpdateStream(context.Context, Notes_UpdateStreamStream) error
}
func RegisterNotesHandler(s server.Server, hdlr NotesHandler, opts ...server.HandlerOption) error {
type notes interface {
List(ctx context.Context, in *ListRequest, out *ListResponse) error
Create(ctx context.Context, in *CreateRequest, out *CreateResponse) error
Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error
Update(ctx context.Context, in *UpdateRequest, out *UpdateResponse) error
UpdateStream(ctx context.Context, stream server.Stream) error
}
type Notes struct {
notes
}
h := &notesHandler{hdlr}
return s.Handle(s.NewHandler(&Notes{h}, opts...))
}
type notesHandler struct {
NotesHandler
}
func (h *notesHandler) List(ctx context.Context, in *ListRequest, out *ListResponse) error {
return h.NotesHandler.List(ctx, in, out)
}
func (h *notesHandler) Create(ctx context.Context, in *CreateRequest, out *CreateResponse) error {
return h.NotesHandler.Create(ctx, in, out)
}
func (h *notesHandler) Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error {
return h.NotesHandler.Delete(ctx, in, out)
}
func (h *notesHandler) Update(ctx context.Context, in *UpdateRequest, out *UpdateResponse) error {
return h.NotesHandler.Update(ctx, in, out)
}
func (h *notesHandler) UpdateStream(ctx context.Context, stream server.Stream) error {
return h.NotesHandler.UpdateStream(ctx, &notesUpdateStreamStream{stream})
}
type Notes_UpdateStreamStream interface {
Context() context.Context
SendMsg(interface{}) error
RecvMsg(interface{}) error
SendAndClose(*UpdateResponse) error
Recv() (*UpdateRequest, error)
}
type notesUpdateStreamStream struct {
stream server.Stream
}
func (x *notesUpdateStreamStream) SendAndClose(in *UpdateResponse) error {
if err := x.SendMsg(in); err != nil {
return err
}
return x.stream.Close()
}
func (x *notesUpdateStreamStream) Context() context.Context {
return x.stream.Context()
}
func (x *notesUpdateStreamStream) SendMsg(m interface{}) error {
return x.stream.Send(m)
}
func (x *notesUpdateStreamStream) RecvMsg(m interface{}) error {
return x.stream.Recv(m)
}
func (x *notesUpdateStreamStream) Recv() (*UpdateRequest, error) {
m := new(UpdateRequest)
if err := x.stream.Recv(m); err != nil {
return nil, err
}
return m, nil
}

View File

@@ -1,48 +0,0 @@
syntax = "proto3";
package notes;
option go_package = "./proto;notes";
service Notes {
rpc List(ListRequest) returns (ListResponse);
rpc Create(CreateRequest) returns (CreateResponse);
rpc Delete(DeleteRequest) returns (DeleteResponse);
rpc Update(UpdateRequest) returns (UpdateResponse);
rpc UpdateStream(stream UpdateRequest) returns (UpdateResponse);
}
message Note {
string id = 1;
int64 created = 2;
string title = 3;
string text = 4;
}
message CreateRequest {
string title = 1;
string text = 2;
}
message CreateResponse {
string id = 1;
}
message UpdateRequest {
string id = 1;
string title = 2;
string text = 3;
}
message UpdateResponse {}
message DeleteRequest {
string id = 1;
}
message DeleteResponse {}
message ListRequest {}
message ListResponse {
repeated Note notes = 1;
}

View File

View File

@@ -1,42 +0,0 @@
Notes service is an RPC service which offers CRUD for notes. It demonstrates usage of the store, errors and logger pacakges.
# Notes Service
## Create a note
```bash
micro notes create --title="HelloWorld" --text="MyFirstNote"
{
"id": "6d3fa5c0-6e79-4418-a72a-c1650efb65d2"
}
```
## Update a note
```bash
micro notes update --id=6d3fa5c0-6e79-4418-a72a-c1650efb65d2 --title="HelloWorld" --text="MyFirstNote (v2)"
{}
```
## List notes
```bash
micro notes list
{
"notes": [
{
"id": "6d3fa5c0-6e79-4418-a72a-c1650efb65d2",
"created": "1602849877",
"title": "HelloWorld",
"text": "MyFirstNote (v2)"
}
]
}
```
## Delete a note
```bash
micro notes delete --id=6d3fa5c0-6e79-4418-a72a-c1650efb65d2
{}
```

2
seen/.gitignore vendored
View File

@@ -1,2 +0,0 @@
seen

View File

@@ -1,3 +0,0 @@
FROM alpine
ADD seen /seen
ENTRYPOINT [ "/seen" ]

View File

@@ -1,27 +0,0 @@
GOPATH:=$(shell go env GOPATH)
.PHONY: init
init:
go get -u github.com/golang/protobuf/proto
go get -u github.com/golang/protobuf/protoc-gen-go
go get github.com/micro/micro/v3/cmd/protoc-gen-micro
.PHONY: proto
proto:
protoc --proto_path=. --micro_out=. --go_out=:. proto/seen.proto
.PHONY: build
build:
go build -o seen *.go
docs:
protoc --openapi_out=. --proto_path=. --micro_out=. --go_out=:. proto/seen.proto
@redoc-cli bundle api-seen.json
.PHONY: test
test:
go test -v ./... -cover
.PHONY: docker
docker:
docker build . -t seen:latest

View File

@@ -1,6 +0,0 @@
Seen is a service to keep track of which resources a user has seen (read). For example, it can be used to keep track of what notifications have been seen by a user, or what messages they've read in a chat.
# Seen Service
The seen service is a service to keep track of which resources a user has seen (read).

View File

@@ -1,221 +0,0 @@
package handler
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/micro/v3/service/store"
"github.com/micro/services/pkg/tenant"
pb "github.com/micro/services/seen/proto"
"google.golang.org/protobuf/types/known/timestamppb"
)
var (
ErrMissingUserID = errors.BadRequest("MISSING_USER_ID", "Missing UserID")
ErrMissingResourceID = errors.BadRequest("MISSING_RESOURCE_ID", "Missing ResourceID")
ErrMissingResourceIDs = errors.BadRequest("MISSING_RESOURCE_IDS", "Missing ResourceIDs")
ErrMissingResourceType = errors.BadRequest("MISSING_RESOURCE_TYPE", "Missing ResourceType")
ErrStore = errors.InternalServerError("STORE_ERROR", "Error connecting to the store")
)
type Seen struct{}
type Record struct {
ID string
UserID string
ResourceID string
ResourceType string
Timestamp time.Time
}
func (r *Record) Key(ctx context.Context) string {
key := fmt.Sprintf("%s:%s:%s", r.UserID, r.ResourceType, r.ResourceID)
t, ok := tenant.FromContext(ctx)
if !ok {
return key
}
return fmt.Sprintf("%s/%s", t, key)
}
func (r *Record) Marshal() []byte {
b, _ := json.Marshal(r)
return b
}
func (r *Record) Unmarshal(b []byte) error {
return json.Unmarshal(b, &r)
}
// Set a resource as seen by a user. If no timestamp is provided, the current time is used.
func (s *Seen) Set(ctx context.Context, req *pb.SetRequest, rsp *pb.SetResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.UserId) == 0 {
return ErrMissingUserID
}
if len(req.ResourceId) == 0 {
return ErrMissingResourceID
}
if len(req.ResourceType) == 0 {
return ErrMissingResourceType
}
// default the timestamp
if req.Timestamp == nil {
req.Timestamp = timestamppb.New(time.Now())
}
// find the resource
instance := &Record{
UserID: req.UserId,
ResourceID: req.ResourceId,
ResourceType: req.ResourceType,
}
_, err := store.Read(instance.Key(ctx), store.ReadLimit(1))
if err == store.ErrNotFound {
instance.ID = uuid.New().String()
} else if err != nil {
logger.Errorf("Error with store: %v", err)
return ErrStore
}
// update the resource
instance.Timestamp = req.Timestamp.AsTime()
if err := store.Write(&store.Record{
Key: instance.Key(ctx),
Value: instance.Marshal(),
}); err != nil {
logger.Errorf("Error with store: %v", err)
return ErrStore
}
return nil
}
// Unset a resource as seen, used in cases where a user viewed a resource but wants to override
// this so they remember to action it in the future, e.g. "Mark this as unread".
func (s *Seen) Unset(ctx context.Context, req *pb.UnsetRequest, rsp *pb.UnsetResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.UserId) == 0 {
return ErrMissingUserID
}
if len(req.ResourceId) == 0 {
return ErrMissingResourceID
}
if len(req.ResourceType) == 0 {
return ErrMissingResourceType
}
instance := &Record{
UserID: req.UserId,
ResourceID: req.ResourceId,
ResourceType: req.ResourceType,
}
// delete the object from the store
if err := store.Delete(instance.Key(ctx)); err != nil {
logger.Errorf("Error with store: %v", err)
return ErrStore
}
return nil
}
// Read returns the timestamps at which various resources were seen by a user. If no timestamp
// is returned for a given resource_id, it indicates that resource has not yet been seen by the
// user.
func (s *Seen) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.UserId) == 0 {
return ErrMissingUserID
}
if len(req.ResourceIds) == 0 {
return ErrMissingResourceIDs
}
if len(req.ResourceType) == 0 {
return ErrMissingResourceType
}
rec := &Record{
UserID: req.UserId,
ResourceType: req.ResourceType,
}
var recs []*store.Record
var err error
// get the records for the resource type
if len(req.ResourceIds) == 1 {
// read the key itself
rec.ResourceID = req.ResourceIds[0]
// gen key
key := rec.Key(ctx)
// get the record
recs, err = store.Read(key, store.ReadLimit(1))
} else {
// create a key prefix
key := rec.Key(ctx)
// otherwise read the prefix
recs, err = store.Read(key, store.ReadPrefix())
}
if err == store.ErrNotFound {
return nil
} else if err != nil {
logger.Errorf("Error with store: %v", err)
return ErrStore
}
// make an id map
ids := make(map[string]bool)
for _, id := range req.ResourceIds {
ids[id] = true
}
// make the map
rsp.Timestamps = make(map[string]*timestamppb.Timestamp)
// range over records for the user/resource type
// TODO: add some sort of filter query in store
for _, rec := range recs {
// get id
parts := strings.Split(rec.Key, ":")
id := parts[2]
fmt.Println("checking record", rec.Key, id)
if ok := ids[id]; !ok {
continue
}
// add the timestamp for the record
r := new(Record)
r.Unmarshal(rec.Value)
rsp.Timestamps[id] = timestamppb.New(r.Timestamp)
}
return nil
}

View File

@@ -1,250 +0,0 @@
package handler_test
import (
"context"
"testing"
"time"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/store"
"github.com/micro/micro/v3/service/store/memory"
"github.com/micro/services/seen/handler"
pb "github.com/micro/services/seen/proto"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/timestamppb"
)
func testHandler(t *testing.T) *handler.Seen {
store.DefaultStore = memory.NewStore()
return &handler.Seen{}
}
func TestSet(t *testing.T) {
tt := []struct {
Name string
UserID string
ResourceType string
ResourceID string
Timestamp *timestamppb.Timestamp
Error error
}{
{
Name: "MissingUserID",
ResourceType: "message",
ResourceID: uuid.New().String(),
Error: handler.ErrMissingUserID,
},
{
Name: "MissingResourceID",
UserID: uuid.New().String(),
ResourceType: "message",
Error: handler.ErrMissingResourceID,
},
{
Name: "MissingResourceType",
UserID: uuid.New().String(),
ResourceID: uuid.New().String(),
Error: handler.ErrMissingResourceType,
},
{
Name: "WithTimetamp",
UserID: uuid.New().String(),
ResourceID: uuid.New().String(),
ResourceType: "message",
Timestamp: timestamppb.New(time.Now().Add(time.Minute * -5)),
},
{
Name: "WithoutTimetamp",
UserID: uuid.New().String(),
ResourceID: uuid.New().String(),
ResourceType: "message",
},
{
Name: "WithUpdatedTimetamp",
UserID: uuid.New().String(),
ResourceID: uuid.New().String(),
ResourceType: "message",
Timestamp: timestamppb.New(time.Now().Add(time.Minute * -3)),
},
}
h := testHandler(t)
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
err := h.Set(microAccountCtx(), &pb.SetRequest{
UserId: tc.UserID,
ResourceId: tc.ResourceID,
ResourceType: tc.ResourceType,
Timestamp: tc.Timestamp,
}, &pb.SetResponse{})
assert.Equal(t, tc.Error, err)
})
}
}
func TestUnset(t *testing.T) {
// seed some test data
h := testHandler(t)
seed := &pb.SetRequest{
UserId: uuid.New().String(),
ResourceId: uuid.New().String(),
ResourceType: "message",
}
err := h.Set(microAccountCtx(), seed, &pb.SetResponse{})
assert.NoError(t, err)
tt := []struct {
Name string
UserID string
ResourceType string
ResourceID string
Error error
}{
{
Name: "MissingUserID",
ResourceType: "message",
ResourceID: uuid.New().String(),
Error: handler.ErrMissingUserID,
},
{
Name: "MissingResourceID",
UserID: uuid.New().String(),
ResourceType: "message",
Error: handler.ErrMissingResourceID,
},
{
Name: "MissingResourceType",
UserID: uuid.New().String(),
ResourceID: uuid.New().String(),
Error: handler.ErrMissingResourceType,
},
{
Name: "Exists",
UserID: seed.UserId,
ResourceID: seed.ResourceId,
ResourceType: seed.ResourceType,
},
{
Name: "Repeat",
UserID: seed.UserId,
ResourceID: seed.ResourceId,
ResourceType: seed.ResourceType,
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
err := h.Unset(microAccountCtx(), &pb.UnsetRequest{
UserId: tc.UserID,
ResourceId: tc.ResourceID,
ResourceType: tc.ResourceType,
}, &pb.UnsetResponse{})
assert.Equal(t, tc.Error, err)
})
}
}
func TestRead(t *testing.T) {
tn := time.Now()
h := testHandler(t)
// seed some test data
td := []struct {
UserID string
ResourceID string
ResourceType string
Timestamp *timestamppb.Timestamp
}{
{
UserID: "user-1",
ResourceID: "message-1",
ResourceType: "message",
Timestamp: timestamppb.New(tn.Add(time.Minute * -10)),
},
{
UserID: "user-1",
ResourceID: "message-1",
ResourceType: "message",
Timestamp: timestamppb.New(tn),
},
{
UserID: "user-1",
ResourceID: "message-2",
ResourceType: "message",
Timestamp: timestamppb.New(tn.Add(time.Minute * -10)),
},
{
UserID: "user-1",
ResourceID: "notification-1",
ResourceType: "notification",
Timestamp: timestamppb.New(tn.Add(time.Minute * -10)),
},
{
UserID: "user-2",
ResourceID: "message-3",
ResourceType: "message",
Timestamp: timestamppb.New(tn.Add(time.Minute * -10)),
},
}
for _, d := range td {
assert.NoError(t, h.Set(microAccountCtx(), &pb.SetRequest{
UserId: d.UserID,
ResourceId: d.ResourceID,
ResourceType: d.ResourceType,
Timestamp: d.Timestamp,
}, &pb.SetResponse{}))
}
// check only the requested values are returned
var rsp pb.ReadResponse
err := h.Read(microAccountCtx(), &pb.ReadRequest{
UserId: "user-1",
ResourceType: "message",
ResourceIds: []string{"message-1", "message-2", "message-3"},
}, &rsp)
assert.NoError(t, err)
assert.Len(t, rsp.Timestamps, 2)
if v := rsp.Timestamps["message-1"]; v != nil {
assert.Equal(t, microSecondTime(v.AsTime()), microSecondTime(tn))
} else {
t.Errorf("Expected a timestamp for message-1")
}
if v := rsp.Timestamps["message-2"]; v != nil {
assert.Equal(t, microSecondTime(v.AsTime()), microSecondTime(tn.Add(time.Minute*-10).UTC()))
} else {
t.Errorf("Expected a timestamp for message-2")
}
// unsetting a resource should remove it from the list
err = h.Unset(microAccountCtx(), &pb.UnsetRequest{
UserId: "user-1",
ResourceId: "message-2",
ResourceType: "message",
}, &pb.UnsetResponse{})
assert.NoError(t, err)
rsp = pb.ReadResponse{}
err = h.Read(microAccountCtx(), &pb.ReadRequest{
UserId: "user-1",
ResourceType: "message",
ResourceIds: []string{"message-1", "message-2", "message-3"},
}, &rsp)
assert.NoError(t, err)
assert.Len(t, rsp.Timestamps, 1)
}
// postgres has a resolution of 100microseconds so just test that it's accurate to the second
func microSecondTime(tt time.Time) time.Time {
return time.Unix(tt.Unix(), int64(tt.Nanosecond()-tt.Nanosecond()%1000)).UTC()
}
func microAccountCtx() context.Context {
return auth.ContextWithAccount(context.TODO(), &auth.Account{
Issuer: "micro",
})
}

View File

@@ -1,24 +0,0 @@
package main
import (
"github.com/micro/micro/v3/service"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/services/seen/handler"
pb "github.com/micro/services/seen/proto"
)
func main() {
// Create service
srv := service.New(
service.Name("seen"),
service.Version("latest"),
)
// Register handler
pb.RegisterSeenHandler(srv.Server(), new(handler.Seen))
// Run service
if err := srv.Run(); err != nil {
logger.Fatal(err)
}
}

View File

@@ -1 +0,0 @@
service seen

View File

@@ -1,614 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.25.0
// protoc v3.15.5
// source: proto/seen.proto
package seen
import (
proto "github.com/golang/protobuf/proto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
type Resource struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"`
}
func (x *Resource) Reset() {
*x = Resource{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_seen_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Resource) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Resource) ProtoMessage() {}
func (x *Resource) ProtoReflect() protoreflect.Message {
mi := &file_proto_seen_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Resource.ProtoReflect.Descriptor instead.
func (*Resource) Descriptor() ([]byte, []int) {
return file_proto_seen_proto_rawDescGZIP(), []int{0}
}
func (x *Resource) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *Resource) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type SetRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
ResourceType string `protobuf:"bytes,2,opt,name=resource_type,json=resourceType,proto3" json:"resource_type,omitempty"`
ResourceId string `protobuf:"bytes,3,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"`
Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
}
func (x *SetRequest) Reset() {
*x = SetRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_seen_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SetRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SetRequest) ProtoMessage() {}
func (x *SetRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_seen_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SetRequest.ProtoReflect.Descriptor instead.
func (*SetRequest) Descriptor() ([]byte, []int) {
return file_proto_seen_proto_rawDescGZIP(), []int{1}
}
func (x *SetRequest) GetUserId() string {
if x != nil {
return x.UserId
}
return ""
}
func (x *SetRequest) GetResourceType() string {
if x != nil {
return x.ResourceType
}
return ""
}
func (x *SetRequest) GetResourceId() string {
if x != nil {
return x.ResourceId
}
return ""
}
func (x *SetRequest) GetTimestamp() *timestamppb.Timestamp {
if x != nil {
return x.Timestamp
}
return nil
}
type SetResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *SetResponse) Reset() {
*x = SetResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_seen_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SetResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SetResponse) ProtoMessage() {}
func (x *SetResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_seen_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SetResponse.ProtoReflect.Descriptor instead.
func (*SetResponse) Descriptor() ([]byte, []int) {
return file_proto_seen_proto_rawDescGZIP(), []int{2}
}
type UnsetRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
ResourceType string `protobuf:"bytes,2,opt,name=resource_type,json=resourceType,proto3" json:"resource_type,omitempty"`
ResourceId string `protobuf:"bytes,3,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"`
}
func (x *UnsetRequest) Reset() {
*x = UnsetRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_seen_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *UnsetRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UnsetRequest) ProtoMessage() {}
func (x *UnsetRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_seen_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UnsetRequest.ProtoReflect.Descriptor instead.
func (*UnsetRequest) Descriptor() ([]byte, []int) {
return file_proto_seen_proto_rawDescGZIP(), []int{3}
}
func (x *UnsetRequest) GetUserId() string {
if x != nil {
return x.UserId
}
return ""
}
func (x *UnsetRequest) GetResourceType() string {
if x != nil {
return x.ResourceType
}
return ""
}
func (x *UnsetRequest) GetResourceId() string {
if x != nil {
return x.ResourceId
}
return ""
}
type UnsetResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *UnsetResponse) Reset() {
*x = UnsetResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_seen_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *UnsetResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UnsetResponse) ProtoMessage() {}
func (x *UnsetResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_seen_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UnsetResponse.ProtoReflect.Descriptor instead.
func (*UnsetResponse) Descriptor() ([]byte, []int) {
return file_proto_seen_proto_rawDescGZIP(), []int{4}
}
type ReadRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
ResourceType string `protobuf:"bytes,2,opt,name=resource_type,json=resourceType,proto3" json:"resource_type,omitempty"`
ResourceIds []string `protobuf:"bytes,3,rep,name=resource_ids,json=resourceIds,proto3" json:"resource_ids,omitempty"`
}
func (x *ReadRequest) Reset() {
*x = ReadRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_seen_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ReadRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ReadRequest) ProtoMessage() {}
func (x *ReadRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_seen_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ReadRequest.ProtoReflect.Descriptor instead.
func (*ReadRequest) Descriptor() ([]byte, []int) {
return file_proto_seen_proto_rawDescGZIP(), []int{5}
}
func (x *ReadRequest) GetUserId() string {
if x != nil {
return x.UserId
}
return ""
}
func (x *ReadRequest) GetResourceType() string {
if x != nil {
return x.ResourceType
}
return ""
}
func (x *ReadRequest) GetResourceIds() []string {
if x != nil {
return x.ResourceIds
}
return nil
}
type ReadResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Timestamps map[string]*timestamppb.Timestamp `protobuf:"bytes,1,rep,name=timestamps,proto3" json:"timestamps,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
func (x *ReadResponse) Reset() {
*x = ReadResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_seen_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ReadResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ReadResponse) ProtoMessage() {}
func (x *ReadResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_seen_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ReadResponse.ProtoReflect.Descriptor instead.
func (*ReadResponse) Descriptor() ([]byte, []int) {
return file_proto_seen_proto_rawDescGZIP(), []int{6}
}
func (x *ReadResponse) GetTimestamps() map[string]*timestamppb.Timestamp {
if x != nil {
return x.Timestamps
}
return nil
}
var File_proto_seen_proto protoreflect.FileDescriptor
var file_proto_seen_proto_rawDesc = []byte{
0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x65, 0x65, 0x6e, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x12, 0x04, 0x73, 0x65, 0x65, 0x6e, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2e, 0x0a, 0x08, 0x52, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0xa5, 0x01, 0x0a, 0x0a, 0x53, 0x65,
0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72,
0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49,
0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79,
0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73,
0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
0x70, 0x22, 0x0d, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x6d, 0x0a, 0x0c, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f,
0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x22,
0x0f, 0x0a, 0x0d, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x6e, 0x0a, 0x0b, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x21, 0x0a,
0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x03, 0x20,
0x03, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x73,
0x22, 0xad, 0x01, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x42, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x18,
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x73, 0x65, 0x65, 0x6e, 0x2e, 0x52, 0x65, 0x61,
0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73,
0x74, 0x61, 0x6d, 0x70, 0x73, 0x1a, 0x59, 0x0a, 0x0f, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
0x6d, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01,
0x32, 0x93, 0x01, 0x0a, 0x04, 0x53, 0x65, 0x65, 0x6e, 0x12, 0x2a, 0x0a, 0x03, 0x53, 0x65, 0x74,
0x12, 0x10, 0x2e, 0x73, 0x65, 0x65, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x11, 0x2e, 0x73, 0x65, 0x65, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x05, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x12, 0x12,
0x2e, 0x73, 0x65, 0x65, 0x6e, 0x2e, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x13, 0x2e, 0x73, 0x65, 0x65, 0x6e, 0x2e, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12,
0x11, 0x2e, 0x73, 0x65, 0x65, 0x6e, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x65, 0x65, 0x6e, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x0c, 0x5a, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x3b,
0x73, 0x65, 0x65, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_proto_seen_proto_rawDescOnce sync.Once
file_proto_seen_proto_rawDescData = file_proto_seen_proto_rawDesc
)
func file_proto_seen_proto_rawDescGZIP() []byte {
file_proto_seen_proto_rawDescOnce.Do(func() {
file_proto_seen_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_seen_proto_rawDescData)
})
return file_proto_seen_proto_rawDescData
}
var file_proto_seen_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_proto_seen_proto_goTypes = []interface{}{
(*Resource)(nil), // 0: seen.Resource
(*SetRequest)(nil), // 1: seen.SetRequest
(*SetResponse)(nil), // 2: seen.SetResponse
(*UnsetRequest)(nil), // 3: seen.UnsetRequest
(*UnsetResponse)(nil), // 4: seen.UnsetResponse
(*ReadRequest)(nil), // 5: seen.ReadRequest
(*ReadResponse)(nil), // 6: seen.ReadResponse
nil, // 7: seen.ReadResponse.TimestampsEntry
(*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp
}
var file_proto_seen_proto_depIdxs = []int32{
8, // 0: seen.SetRequest.timestamp:type_name -> google.protobuf.Timestamp
7, // 1: seen.ReadResponse.timestamps:type_name -> seen.ReadResponse.TimestampsEntry
8, // 2: seen.ReadResponse.TimestampsEntry.value:type_name -> google.protobuf.Timestamp
1, // 3: seen.Seen.Set:input_type -> seen.SetRequest
3, // 4: seen.Seen.Unset:input_type -> seen.UnsetRequest
5, // 5: seen.Seen.Read:input_type -> seen.ReadRequest
2, // 6: seen.Seen.Set:output_type -> seen.SetResponse
4, // 7: seen.Seen.Unset:output_type -> seen.UnsetResponse
6, // 8: seen.Seen.Read:output_type -> seen.ReadResponse
6, // [6:9] is the sub-list for method output_type
3, // [3:6] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_proto_seen_proto_init() }
func file_proto_seen_proto_init() {
if File_proto_seen_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_proto_seen_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Resource); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_seen_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SetRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_seen_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SetResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_seen_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*UnsetRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_seen_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*UnsetResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_seen_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ReadRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_seen_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ReadResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_seen_proto_rawDesc,
NumEnums: 0,
NumMessages: 8,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_proto_seen_proto_goTypes,
DependencyIndexes: file_proto_seen_proto_depIdxs,
MessageInfos: file_proto_seen_proto_msgTypes,
}.Build()
File_proto_seen_proto = out.File
file_proto_seen_proto_rawDesc = nil
file_proto_seen_proto_goTypes = nil
file_proto_seen_proto_depIdxs = nil
}

View File

@@ -1,140 +0,0 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: proto/seen.proto
package seen
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
_ "google.golang.org/protobuf/types/known/timestamppb"
math "math"
)
import (
context "context"
api "github.com/micro/micro/v3/service/api"
client "github.com/micro/micro/v3/service/client"
server "github.com/micro/micro/v3/service/server"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
// Reference imports to suppress errors if they are not otherwise used.
var _ api.Endpoint
var _ context.Context
var _ client.Option
var _ server.Option
// Api Endpoints for Seen service
func NewSeenEndpoints() []*api.Endpoint {
return []*api.Endpoint{}
}
// Client API for Seen service
type SeenService interface {
// Set a resource as seen by a user. If no timestamp is provided, the current time is used.
Set(ctx context.Context, in *SetRequest, opts ...client.CallOption) (*SetResponse, error)
// Unset a resource as seen, used in cases where a user viewed a resource but wants to override
// this so they remember to action it in the future, e.g. "Mark this as unread".
Unset(ctx context.Context, in *UnsetRequest, opts ...client.CallOption) (*UnsetResponse, error)
// Read returns the timestamps at which various resources were seen by a user. If no timestamp
// is returned for a given resource_id, it indicates that resource has not yet been seen by the
// user.
Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ReadResponse, error)
}
type seenService struct {
c client.Client
name string
}
func NewSeenService(name string, c client.Client) SeenService {
return &seenService{
c: c,
name: name,
}
}
func (c *seenService) Set(ctx context.Context, in *SetRequest, opts ...client.CallOption) (*SetResponse, error) {
req := c.c.NewRequest(c.name, "Seen.Set", in)
out := new(SetResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *seenService) Unset(ctx context.Context, in *UnsetRequest, opts ...client.CallOption) (*UnsetResponse, error) {
req := c.c.NewRequest(c.name, "Seen.Unset", in)
out := new(UnsetResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *seenService) Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ReadResponse, error) {
req := c.c.NewRequest(c.name, "Seen.Read", in)
out := new(ReadResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Seen service
type SeenHandler interface {
// Set a resource as seen by a user. If no timestamp is provided, the current time is used.
Set(context.Context, *SetRequest, *SetResponse) error
// Unset a resource as seen, used in cases where a user viewed a resource but wants to override
// this so they remember to action it in the future, e.g. "Mark this as unread".
Unset(context.Context, *UnsetRequest, *UnsetResponse) error
// Read returns the timestamps at which various resources were seen by a user. If no timestamp
// is returned for a given resource_id, it indicates that resource has not yet been seen by the
// user.
Read(context.Context, *ReadRequest, *ReadResponse) error
}
func RegisterSeenHandler(s server.Server, hdlr SeenHandler, opts ...server.HandlerOption) error {
type seen interface {
Set(ctx context.Context, in *SetRequest, out *SetResponse) error
Unset(ctx context.Context, in *UnsetRequest, out *UnsetResponse) error
Read(ctx context.Context, in *ReadRequest, out *ReadResponse) error
}
type Seen struct {
seen
}
h := &seenHandler{hdlr}
return s.Handle(s.NewHandler(&Seen{h}, opts...))
}
type seenHandler struct {
SeenHandler
}
func (h *seenHandler) Set(ctx context.Context, in *SetRequest, out *SetResponse) error {
return h.SeenHandler.Set(ctx, in, out)
}
func (h *seenHandler) Unset(ctx context.Context, in *UnsetRequest, out *UnsetResponse) error {
return h.SeenHandler.Unset(ctx, in, out)
}
func (h *seenHandler) Read(ctx context.Context, in *ReadRequest, out *ReadResponse) error {
return h.SeenHandler.Read(ctx, in, out)
}

View File

@@ -1,52 +0,0 @@
syntax = "proto3";
package seen;
option go_package = "./proto;seen";
import "google/protobuf/timestamp.proto";
// Seen is a service to keep track of which resources a user has seen (read). For example, it can
// be used to keep track of what notifications have been seen by a user, or what messages they've
// read in a chat.
service Seen {
// Set a resource as seen by a user. If no timestamp is provided, the current time is used.
rpc Set(SetRequest) returns (SetResponse);
// Unset a resource as seen, used in cases where a user viewed a resource but wants to override
// this so they remember to action it in the future, e.g. "Mark this as unread".
rpc Unset(UnsetRequest) returns (UnsetResponse);
// Read returns the timestamps at which various resources were seen by a user. If no timestamp
// is returned for a given resource_id, it indicates that resource has not yet been seen by the
// user.
rpc Read(ReadRequest) returns (ReadResponse);
}
message Resource {
string type = 1;
string id = 2;
}
message SetRequest {
string user_id = 1;
string resource_type = 2;
string resource_id = 3;
google.protobuf.Timestamp timestamp = 4;
}
message SetResponse {}
message UnsetRequest {
string user_id = 1;
string resource_type = 2;
string resource_id = 3;
}
message UnsetResponse {}
message ReadRequest {
string user_id = 1;
string resource_type = 2;
repeated string resource_ids = 3;
}
message ReadResponse {
map<string, google.protobuf.Timestamp> timestamps = 1;
}

2
streams/.gitignore vendored
View File

@@ -1,2 +0,0 @@
streams

View File

@@ -1,27 +0,0 @@
GOPATH:=$(shell go env GOPATH)
.PHONY: init
init:
go get -u github.com/golang/protobuf/proto
go get -u github.com/golang/protobuf/protoc-gen-go
go get github.com/micro/micro/v3/cmd/protoc-gen-micro
.PHONY: proto
proto:
protoc --proto_path=. --micro_out=. --go_out=:. proto/streams.proto
.PHONY: build
build:
go build -o streams *.go
.PHONY: docs
docs:
protoc --openapi_out=. --proto_path=. --micro_out=. --go_out=:. proto/streams.proto
@redoc-cli bundle api-streams.json
.PHONY: test
test:
go test -v ./... -cover
.PHONY: docker
docker:
docker build . -t streams:latest

View File

@@ -1,7 +0,0 @@
PubSub streaming and websockets
# Streams Service
The streams service provides an event stream, designed for sending messages from a server to mutliple
clients connecting via Websockets. The Token RPC should be called to generate a token for each client,
the clients should then subscribe using the Subscribe RPC.

View File

@@ -1,57 +0,0 @@
package handler
import (
"fmt"
"time"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/events"
"github.com/micro/services/pkg/cache"
"github.com/nats-io/nats-streaming-server/util"
)
var (
TokenTTL = time.Minute
ErrMissingTopic = errors.BadRequest("MISSING_TOPIC", "Missing topic")
ErrInvalidTopic = errors.BadRequest("MISSING_TOPIC", "Invalid topic")
ErrMissingToken = errors.BadRequest("MISSING_TOKEN", "Missing token")
ErrMissingMessage = errors.BadRequest("MISSING_MESSAGE", "Missing message")
ErrInvalidToken = errors.Forbidden("INVALID_TOKEN", "Invalid token")
ErrExpiredToken = errors.Forbidden("EXPIRED_TOKEN", "Token expired")
ErrForbiddenTopic = errors.Forbidden("FORBIDDEN_TOPIC", "Token has not have permission to subscribe to this topic")
)
type Token struct {
Token string
Topic string
Account string
ExpiresAt time.Time
}
type Streams struct {
Cache cache.Cache
Events events.Stream
Time func() time.Time
}
func getAccount(acc *auth.Account) string {
owner := acc.Metadata["apikey_owner"]
if len(owner) == 0 {
owner = acc.ID
}
return fmt.Sprintf("%s.%s", acc.Issuer, owner)
}
// fmtTopic returns a topic string with namespace prefix
func fmtTopic(acc *auth.Account, topic string) string {
return fmt.Sprintf("%s.%s", getAccount(acc), topic)
}
// validateTopicInput validates that topic is alphanumeric
func validateTopicInput(topic string) error {
if !util.IsChannelNameValid(topic, false) {
return ErrInvalidTopic
}
return nil
}

View File

@@ -1,43 +0,0 @@
package handler_test
import (
"testing"
"time"
"github.com/micro/micro/v3/service/events"
"github.com/micro/micro/v3/service/store/memory"
"github.com/micro/services/pkg/cache"
"github.com/micro/services/streams/handler"
)
func testHandler(t *testing.T) *handler.Streams {
h := &handler.Streams{
Cache: cache.New(memory.NewStore()),
Events: new(eventsMock),
Time: func() time.Time {
return time.Unix(1612787045, 0)
},
}
return h
}
type eventsMock struct {
PublishCount int
PublishTopic string
PublishMessage interface{}
ConsumeTopic string
ConsumeChan <-chan events.Event
}
func (e *eventsMock) Publish(topic string, msg interface{}, opts ...events.PublishOption) error {
e.PublishCount++
e.PublishTopic = topic
e.PublishMessage = msg
return nil
}
func (e *eventsMock) Consume(topic string, opts ...events.ConsumeOption) (<-chan events.Event, error) {
e.ConsumeTopic = topic
return e.ConsumeChan, nil
}

View File

@@ -1,34 +0,0 @@
package handler
import (
"context"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/logger"
pb "github.com/micro/services/streams/proto"
)
func (s *Streams) Publish(ctx context.Context, req *pb.Message, rsp *pb.PublishResponse) error {
// validate the request
if len(req.Topic) == 0 {
return ErrMissingTopic
}
if err := validateTopicInput(req.Topic); err != nil {
return err
}
if len(req.Message) == 0 {
return ErrMissingMessage
}
topic := req.Topic
// in the event we have an account we use multi-tenancy
acc, ok := auth.AccountFromContext(ctx)
if ok {
topic = fmtTopic(acc, req.Topic)
}
// publish the message
logger.Infof("Publishing message to topic: %v", req.Topic)
return s.Events.Publish(topic, req.Message)
}

View File

@@ -1,47 +0,0 @@
package handler_test
import (
"context"
"strings"
"testing"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/services/streams/handler"
pb "github.com/micro/services/streams/proto"
"github.com/stretchr/testify/assert"
)
func TestPublish(t *testing.T) {
msg := "{\"foo\":\"bar\"}"
topic := strings.ReplaceAll(uuid.New().String(), "-", "")
t.Run("MissingTopic", func(t *testing.T) {
h := testHandler(t)
ctx := auth.ContextWithAccount(context.TODO(), &auth.Account{Issuer: "foo"})
err := h.Publish(ctx, &pb.Message{Message: msg}, &pb.PublishResponse{})
assert.Equal(t, handler.ErrMissingTopic, err)
assert.Zero(t, h.Events.(*eventsMock).PublishCount)
})
t.Run("MissingMessage", func(t *testing.T) {
h := testHandler(t)
ctx := auth.ContextWithAccount(context.TODO(), &auth.Account{Issuer: "foo"})
err := h.Publish(ctx, &pb.Message{Topic: topic}, &pb.PublishResponse{})
assert.Equal(t, handler.ErrMissingMessage, err)
assert.Zero(t, h.Events.(*eventsMock).PublishCount)
})
t.Run("ValidMessage", func(t *testing.T) {
h := testHandler(t)
ctx := auth.ContextWithAccount(context.TODO(), &auth.Account{Issuer: "foo", ID: "foo-id"})
err := h.Publish(ctx, &pb.Message{
Topic: topic, Message: msg,
}, &pb.PublishResponse{})
assert.NoError(t, err)
assert.Equal(t, 1, h.Events.(*eventsMock).PublishCount)
assert.Equal(t, msg, h.Events.(*eventsMock).PublishMessage)
// topic is prefixed with acc issuer to implement multitenancy
assert.Equal(t, "foo.foo-id."+topic, h.Events.(*eventsMock).PublishTopic)
})
}

View File

@@ -1,89 +0,0 @@
package handler
import (
"context"
"fmt"
"io"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/events"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/micro/v3/service/store"
pb "github.com/micro/services/streams/proto"
"google.golang.org/protobuf/types/known/timestamppb"
)
func (s *Streams) Subscribe(ctx context.Context, req *pb.SubscribeRequest, stream pb.Streams_SubscribeStream) error {
logger.Infof("Received subscribe request. Topic: '%v', Token: '%v'", req.Topic, req.Token)
// validate the request
if len(req.Token) == 0 {
return ErrMissingToken
}
if len(req.Topic) == 0 {
return ErrMissingTopic
}
if err := validateTopicInput(req.Topic); err != nil {
return err
}
// find the token and check to see if it has expired
var token Token
if err := s.Cache.Get("token:"+req.Token, &token); err == store.ErrNotFound {
return ErrInvalidToken
} else if err != nil {
logger.Errorf("Error reading token from store: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error reading token from database")
}
if token.ExpiresAt.Before(s.Time()) {
return ErrExpiredToken
}
// if the token was scoped to a channel, ensure the channel is the one being requested
if len(token.Topic) > 0 && token.Topic != req.Topic {
return ErrForbiddenTopic
}
var topic string
// attempt to create a unique topic for the account
acc, ok := auth.AccountFromContext(ctx)
if ok {
topic = fmtTopic(acc, req.Topic)
} else if len(token.Account) > 0 {
// use the account in the token if present
topic = fmt.Sprintf("%s.%s", token.Account, token.Topic)
} else {
topic = req.Topic
}
// start the subscription
logger.Infof("Subscribing to %v via queue %v", req.Topic, token.Token)
evChan, err := s.Events.Consume(topic, events.WithGroup(token.Token))
if err != nil {
logger.Errorf("Error connecting to events stream: %v", err)
return errors.InternalServerError("EVENTS_ERROR", "Error connecting to events stream")
}
for {
msg, ok := <-evChan
if !ok {
return nil
}
logger.Infof("Sending message to subscriber %v", token.Topic)
pbMsg := &pb.Message{
Topic: req.Topic, // use req.Topic not msg.Topic because topic is munged for multitenancy
Message: string(msg.Payload),
SentAt: timestamppb.New(msg.Timestamp),
}
if err := stream.Send(pbMsg); err == io.EOF {
return nil
} else if err != nil {
return err
}
}
}

View File

@@ -1,228 +0,0 @@
package handler_test
import (
"context"
"sync"
"testing"
"time"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/events"
"github.com/micro/services/streams/handler"
pb "github.com/micro/services/streams/proto"
"github.com/stretchr/testify/assert"
)
func TestSubscribe(t *testing.T) {
t.Run("MissingToken", func(t *testing.T) {
h := testHandler(t)
s := new(streamMock)
ctx := auth.ContextWithAccount(context.TODO(), &auth.Account{Issuer: "foo"})
err := h.Subscribe(ctx, &pb.SubscribeRequest{
Topic: "helloworld",
}, s)
assert.Equal(t, handler.ErrMissingToken, err)
assert.Empty(t, s.Messages)
})
t.Run("MissingTopic", func(t *testing.T) {
h := testHandler(t)
s := new(streamMock)
ctx := auth.ContextWithAccount(context.TODO(), &auth.Account{Issuer: "foo"})
err := h.Subscribe(ctx, &pb.SubscribeRequest{
Token: uuid.New().String(),
}, s)
assert.Equal(t, handler.ErrMissingTopic, err)
assert.Empty(t, s.Messages)
})
t.Run("InvalidToken", func(t *testing.T) {
h := testHandler(t)
s := new(streamMock)
ctx := auth.ContextWithAccount(context.TODO(), &auth.Account{Issuer: "foo"})
err := h.Subscribe(ctx, &pb.SubscribeRequest{
Topic: "helloworld",
Token: uuid.New().String(),
}, s)
assert.Equal(t, handler.ErrInvalidToken, err)
assert.Empty(t, s.Messages)
})
t.Run("ExpiredToken", func(t *testing.T) {
h := testHandler(t)
var tRsp pb.TokenResponse
ctx := auth.ContextWithAccount(context.TODO(), &auth.Account{Issuer: "foo"})
err := h.Token(ctx, &pb.TokenRequest{
Topic: "helloworld",
}, &tRsp)
assert.NoError(t, err)
ct := h.Time()
h.Time = func() time.Time { return ct.Add(handler.TokenTTL * 2) }
s := new(streamMock)
err = h.Subscribe(ctx, &pb.SubscribeRequest{
Topic: "helloworld",
Token: tRsp.Token,
}, s)
assert.Equal(t, handler.ErrExpiredToken, err)
assert.Empty(t, s.Messages)
})
t.Run("ForbiddenTopic", func(t *testing.T) {
h := testHandler(t)
var tRsp pb.TokenResponse
ctx := auth.ContextWithAccount(context.TODO(), &auth.Account{Issuer: "foo"})
err := h.Token(ctx, &pb.TokenRequest{
Topic: "helloworldx",
}, &tRsp)
assert.NoError(t, err)
s := new(streamMock)
err = h.Subscribe(ctx, &pb.SubscribeRequest{
Topic: "helloworld",
Token: tRsp.Token,
}, s)
assert.Equal(t, handler.ErrForbiddenTopic, err)
assert.Empty(t, s.Messages)
})
t.Run("Valid", func(t *testing.T) {
defer func() {
if i := recover(); i != nil {
t.Logf("%+v", i)
}
}()
h := testHandler(t)
c := make(chan events.Event)
h.Events.(*eventsMock).ConsumeChan = c
var tRsp pb.TokenResponse
ctx := auth.ContextWithAccount(context.TODO(), &auth.Account{ID: "foo", Issuer: "my-ns"})
err := h.Token(ctx, &pb.TokenRequest{
Topic: "helloworld",
}, &tRsp)
assert.NoError(t, err)
s := &streamMock{Messages: []*pb.Message{}}
wg := sync.WaitGroup{}
wg.Add(1)
var subsErr error
go func() {
defer wg.Done()
subsErr = h.Subscribe(ctx, &pb.SubscribeRequest{
Topic: "helloworld",
Token: tRsp.Token,
}, s)
}()
e1 := events.Event{
ID: uuid.New().String(),
Topic: "helloworld",
Timestamp: h.Time().Add(time.Second * -2),
Payload: []byte("abc"),
}
e2 := events.Event{
ID: uuid.New().String(),
Topic: "helloworld",
Timestamp: h.Time().Add(time.Second * -1),
Payload: []byte("123"),
}
timeout := time.NewTimer(time.Millisecond * 100).C
select {
case <-timeout:
t.Fatal("Events not consumed from stream")
return
case c <- e1:
t.Log("Event1 consumed")
}
select {
case <-timeout:
t.Fatal("Events not consumed from stream")
return
case c <- e2:
t.Log("Event2 consumed")
}
close(c)
wg.Wait()
assert.NoError(t, subsErr)
assert.Equal(t, "my-ns.foo.helloworld", h.Events.(*eventsMock).ConsumeTopic)
// sleep to wait for the subscribe loop to push the message to the stream
//time.Sleep(1 * time.Second)
if len(s.Messages) != 2 {
t.Fatalf("Expected 2 messages, got %v", len(s.Messages))
return
}
assert.Equal(t, e1.Topic, s.Messages[0].Topic)
assert.Equal(t, string(e1.Payload), s.Messages[0].Message)
assert.True(t, e1.Timestamp.Equal(s.Messages[0].SentAt.AsTime()))
assert.Equal(t, e2.Topic, s.Messages[1].Topic)
assert.Equal(t, string(e2.Payload), s.Messages[1].Message)
assert.True(t, e2.Timestamp.Equal(s.Messages[1].SentAt.AsTime()))
})
/*
t.Run("TokenForDifferentIssuer", func(t *testing.T) {
h := testHandler(t)
var tRsp pb.TokenResponse
ctx := auth.ContextWithAccount(context.TODO(), &auth.Account{Issuer: "foo"})
err := h.Token(ctx, &pb.TokenRequest{
Topic: "tokfordiff",
}, &tRsp)
assert.NoError(t, err)
s := new(streamMock)
ctx = auth.ContextWithAccount(context.TODO(), &auth.Account{Issuer: "bar"})
err = h.Subscribe(ctx, &pb.SubscribeRequest{
Topic: "tokfordiff",
Token: tRsp.Token,
}, s)
assert.Equal(t, handler.ErrInvalidToken, err)
assert.Empty(t, s.Messages)
})
*/
t.Run("BadTopic", func(t *testing.T) {
h := testHandler(t)
var tRsp pb.TokenResponse
ctx := auth.ContextWithAccount(context.TODO(), &auth.Account{Issuer: "foo"})
err := h.Token(ctx, &pb.TokenRequest{}, &tRsp)
assert.NoError(t, err)
s := new(streamMock)
ctx = auth.ContextWithAccount(context.TODO(), &auth.Account{Issuer: "bar"})
err = h.Subscribe(ctx, &pb.SubscribeRequest{
Topic: "tok-for/diff",
Token: tRsp.Token,
}, s)
assert.Equal(t, handler.ErrInvalidTopic, err)
assert.Empty(t, s.Messages)
})
}
type streamMock struct {
Messages []*pb.Message
pb.Streams_SubscribeStream
}
func (x *streamMock) Send(m *pb.Message) error {
x.Messages = append(x.Messages, m)
return nil
}

View File

@@ -1,41 +0,0 @@
package handler
import (
"context"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
pb "github.com/micro/services/streams/proto"
)
func (s *Streams) Token(ctx context.Context, req *pb.TokenRequest, rsp *pb.TokenResponse) error {
if len(req.Topic) > 0 {
if err := validateTopicInput(req.Topic); err != nil {
return err
}
}
var account string
if acc, ok := auth.AccountFromContext(ctx); ok {
account = getAccount(acc)
}
// construct the token and write it to the database
t := Token{
Token: uuid.New().String(),
ExpiresAt: s.Time().Add(TokenTTL),
Topic: req.Topic,
Account: account,
}
if err := s.Cache.Put("token:"+t.Token, t, t.ExpiresAt); err != nil {
logger.Errorf("Error creating token in store: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error writing token to database")
}
// return the token in the response
rsp.Token = t.Token
return nil
}

View File

@@ -1,38 +0,0 @@
package handler_test
import (
"context"
"testing"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/services/streams/handler"
pb "github.com/micro/services/streams/proto"
"github.com/stretchr/testify/assert"
)
func TestToken(t *testing.T) {
h := testHandler(t)
t.Run("WithoutTopic", func(t *testing.T) {
var rsp pb.TokenResponse
ctx := auth.ContextWithAccount(context.TODO(), &auth.Account{Issuer: "foo"})
err := h.Token(ctx, &pb.TokenRequest{}, &rsp)
assert.NoError(t, err)
assert.NotEmpty(t, rsp.Token)
})
t.Run("WithTopic", func(t *testing.T) {
var rsp pb.TokenResponse
ctx := auth.ContextWithAccount(context.TODO(), &auth.Account{Issuer: "foo"})
err := h.Token(ctx, &pb.TokenRequest{Topic: "helloworld"}, &rsp)
assert.NoError(t, err)
assert.NotEmpty(t, rsp.Token)
})
t.Run("WithBadTopic", func(t *testing.T) {
var rsp pb.TokenResponse
ctx := auth.ContextWithAccount(context.TODO(), &auth.Account{Issuer: "foo"})
err := h.Token(ctx, &pb.TokenRequest{Topic: "helloworld/1"}, &rsp)
assert.Equal(t, handler.ErrInvalidTopic, err)
})
}

View File

@@ -1,33 +0,0 @@
package main
import (
"time"
"github.com/micro/micro/v3/service"
"github.com/micro/micro/v3/service/events"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/services/pkg/cache"
"github.com/micro/services/streams/handler"
pb "github.com/micro/services/streams/proto"
)
func main() {
// Create service
srv := service.New(
service.Name("streams"),
)
h := &handler.Streams{
Cache: cache.DefaultCache,
Events: events.DefaultStream,
Time: time.Now,
}
// Register handler
pb.RegisterStreamsHandler(srv.Server(), h)
// Run service
if err := srv.Run(); err != nil {
logger.Fatal(err)
}
}

View File

@@ -1 +0,0 @@
service streams

View File

@@ -1,441 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v3.15.5
// source: proto/streams.proto
package streams
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type PublishResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *PublishResponse) Reset() {
*x = PublishResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_streams_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *PublishResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PublishResponse) ProtoMessage() {}
func (x *PublishResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_streams_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PublishResponse.ProtoReflect.Descriptor instead.
func (*PublishResponse) Descriptor() ([]byte, []int) {
return file_proto_streams_proto_rawDescGZIP(), []int{0}
}
type Message struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Topic string `protobuf:"bytes,1,opt,name=topic,proto3" json:"topic,omitempty"`
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
SentAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=sent_at,json=sentAt,proto3" json:"sent_at,omitempty"`
}
func (x *Message) Reset() {
*x = Message{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_streams_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Message) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Message) ProtoMessage() {}
func (x *Message) ProtoReflect() protoreflect.Message {
mi := &file_proto_streams_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Message.ProtoReflect.Descriptor instead.
func (*Message) Descriptor() ([]byte, []int) {
return file_proto_streams_proto_rawDescGZIP(), []int{1}
}
func (x *Message) GetTopic() string {
if x != nil {
return x.Topic
}
return ""
}
func (x *Message) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func (x *Message) GetSentAt() *timestamppb.Timestamp {
if x != nil {
return x.SentAt
}
return nil
}
type SubscribeRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// topic the user wishes to subscribe to, required
Topic string `protobuf:"bytes,1,opt,name=topic,proto3" json:"topic,omitempty"`
// tokens should be provided if the user is not proving an API key on the request (e.g. in cases
// where the stream is being consumed directly from the frontend via websockets). tokens can be
// generated using the Token RPC
Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"`
}
func (x *SubscribeRequest) Reset() {
*x = SubscribeRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_streams_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SubscribeRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SubscribeRequest) ProtoMessage() {}
func (x *SubscribeRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_streams_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SubscribeRequest.ProtoReflect.Descriptor instead.
func (*SubscribeRequest) Descriptor() ([]byte, []int) {
return file_proto_streams_proto_rawDescGZIP(), []int{2}
}
func (x *SubscribeRequest) GetTopic() string {
if x != nil {
return x.Topic
}
return ""
}
func (x *SubscribeRequest) GetToken() string {
if x != nil {
return x.Token
}
return ""
}
type TokenRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// the topic the token should be restricted to, if no topic is required the token can be used to
// subscribe to any topic
Topic string `protobuf:"bytes,1,opt,name=topic,proto3" json:"topic,omitempty"`
}
func (x *TokenRequest) Reset() {
*x = TokenRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_streams_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *TokenRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TokenRequest) ProtoMessage() {}
func (x *TokenRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_streams_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TokenRequest.ProtoReflect.Descriptor instead.
func (*TokenRequest) Descriptor() ([]byte, []int) {
return file_proto_streams_proto_rawDescGZIP(), []int{3}
}
func (x *TokenRequest) GetTopic() string {
if x != nil {
return x.Topic
}
return ""
}
type TokenResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
}
func (x *TokenResponse) Reset() {
*x = TokenResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_streams_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *TokenResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TokenResponse) ProtoMessage() {}
func (x *TokenResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_streams_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead.
func (*TokenResponse) Descriptor() ([]byte, []int) {
return file_proto_streams_proto_rawDescGZIP(), []int{4}
}
func (x *TokenResponse) GetToken() string {
if x != nil {
return x.Token
}
return ""
}
var File_proto_streams_proto protoreflect.FileDescriptor
var file_proto_streams_proto_rawDesc = []byte{
0x0a, 0x13, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x1a, 0x1f,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f,
0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
0x11, 0x0a, 0x0f, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x22, 0x6e, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x14, 0x0a,
0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f,
0x70, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x33, 0x0a,
0x07, 0x73, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x73, 0x65, 0x6e, 0x74,
0x41, 0x74, 0x22, 0x3e, 0x0a, 0x10, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x14, 0x0a, 0x05,
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b,
0x65, 0x6e, 0x22, 0x24, 0x0a, 0x0c, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x22, 0x25, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65,
0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b,
0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x32,
0xba, 0x01, 0x0a, 0x07, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x12, 0x37, 0x0a, 0x07, 0x50,
0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x12, 0x10, 0x2e, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73,
0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x18, 0x2e, 0x73, 0x74, 0x72, 0x65, 0x61,
0x6d, 0x73, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x22, 0x00, 0x12, 0x3c, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62,
0x65, 0x12, 0x19, 0x2e, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x2e, 0x53, 0x75, 0x62, 0x73,
0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x73,
0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00,
0x30, 0x01, 0x12, 0x38, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x15, 0x2e, 0x73, 0x74,
0x72, 0x65, 0x61, 0x6d, 0x73, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x2e, 0x54, 0x6f, 0x6b,
0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x11, 0x5a, 0x0f,
0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x3b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_proto_streams_proto_rawDescOnce sync.Once
file_proto_streams_proto_rawDescData = file_proto_streams_proto_rawDesc
)
func file_proto_streams_proto_rawDescGZIP() []byte {
file_proto_streams_proto_rawDescOnce.Do(func() {
file_proto_streams_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_streams_proto_rawDescData)
})
return file_proto_streams_proto_rawDescData
}
var file_proto_streams_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_proto_streams_proto_goTypes = []interface{}{
(*PublishResponse)(nil), // 0: streams.PublishResponse
(*Message)(nil), // 1: streams.Message
(*SubscribeRequest)(nil), // 2: streams.SubscribeRequest
(*TokenRequest)(nil), // 3: streams.TokenRequest
(*TokenResponse)(nil), // 4: streams.TokenResponse
(*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp
}
var file_proto_streams_proto_depIdxs = []int32{
5, // 0: streams.Message.sent_at:type_name -> google.protobuf.Timestamp
1, // 1: streams.Streams.Publish:input_type -> streams.Message
2, // 2: streams.Streams.Subscribe:input_type -> streams.SubscribeRequest
3, // 3: streams.Streams.Token:input_type -> streams.TokenRequest
0, // 4: streams.Streams.Publish:output_type -> streams.PublishResponse
1, // 5: streams.Streams.Subscribe:output_type -> streams.Message
4, // 6: streams.Streams.Token:output_type -> streams.TokenResponse
4, // [4:7] is the sub-list for method output_type
1, // [1:4] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_proto_streams_proto_init() }
func file_proto_streams_proto_init() {
if File_proto_streams_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_proto_streams_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*PublishResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_streams_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Message); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_streams_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SubscribeRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_streams_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*TokenRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_streams_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*TokenResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_streams_proto_rawDesc,
NumEnums: 0,
NumMessages: 5,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_proto_streams_proto_goTypes,
DependencyIndexes: file_proto_streams_proto_depIdxs,
MessageInfos: file_proto_streams_proto_msgTypes,
}.Build()
File_proto_streams_proto = out.File
file_proto_streams_proto_rawDesc = nil
file_proto_streams_proto_goTypes = nil
file_proto_streams_proto_depIdxs = nil
}

View File

@@ -1,203 +0,0 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: proto/streams.proto
package streams
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
_ "google.golang.org/protobuf/types/known/timestamppb"
math "math"
)
import (
context "context"
api "github.com/micro/micro/v3/service/api"
client "github.com/micro/micro/v3/service/client"
server "github.com/micro/micro/v3/service/server"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
// Reference imports to suppress errors if they are not otherwise used.
var _ api.Endpoint
var _ context.Context
var _ client.Option
var _ server.Option
// Api Endpoints for Streams service
func NewStreamsEndpoints() []*api.Endpoint {
return []*api.Endpoint{}
}
// Client API for Streams service
type StreamsService interface {
Publish(ctx context.Context, in *Message, opts ...client.CallOption) (*PublishResponse, error)
Subscribe(ctx context.Context, in *SubscribeRequest, opts ...client.CallOption) (Streams_SubscribeService, error)
Token(ctx context.Context, in *TokenRequest, opts ...client.CallOption) (*TokenResponse, error)
}
type streamsService struct {
c client.Client
name string
}
func NewStreamsService(name string, c client.Client) StreamsService {
return &streamsService{
c: c,
name: name,
}
}
func (c *streamsService) Publish(ctx context.Context, in *Message, opts ...client.CallOption) (*PublishResponse, error) {
req := c.c.NewRequest(c.name, "Streams.Publish", in)
out := new(PublishResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *streamsService) Subscribe(ctx context.Context, in *SubscribeRequest, opts ...client.CallOption) (Streams_SubscribeService, error) {
req := c.c.NewRequest(c.name, "Streams.Subscribe", &SubscribeRequest{})
stream, err := c.c.Stream(ctx, req, opts...)
if err != nil {
return nil, err
}
if err := stream.Send(in); err != nil {
return nil, err
}
return &streamsServiceSubscribe{stream}, nil
}
type Streams_SubscribeService interface {
Context() context.Context
SendMsg(interface{}) error
RecvMsg(interface{}) error
Close() error
Recv() (*Message, error)
}
type streamsServiceSubscribe struct {
stream client.Stream
}
func (x *streamsServiceSubscribe) Close() error {
return x.stream.Close()
}
func (x *streamsServiceSubscribe) Context() context.Context {
return x.stream.Context()
}
func (x *streamsServiceSubscribe) SendMsg(m interface{}) error {
return x.stream.Send(m)
}
func (x *streamsServiceSubscribe) RecvMsg(m interface{}) error {
return x.stream.Recv(m)
}
func (x *streamsServiceSubscribe) Recv() (*Message, error) {
m := new(Message)
err := x.stream.Recv(m)
if err != nil {
return nil, err
}
return m, nil
}
func (c *streamsService) Token(ctx context.Context, in *TokenRequest, opts ...client.CallOption) (*TokenResponse, error) {
req := c.c.NewRequest(c.name, "Streams.Token", in)
out := new(TokenResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Streams service
type StreamsHandler interface {
Publish(context.Context, *Message, *PublishResponse) error
Subscribe(context.Context, *SubscribeRequest, Streams_SubscribeStream) error
Token(context.Context, *TokenRequest, *TokenResponse) error
}
func RegisterStreamsHandler(s server.Server, hdlr StreamsHandler, opts ...server.HandlerOption) error {
type streams interface {
Publish(ctx context.Context, in *Message, out *PublishResponse) error
Subscribe(ctx context.Context, stream server.Stream) error
Token(ctx context.Context, in *TokenRequest, out *TokenResponse) error
}
type Streams struct {
streams
}
h := &streamsHandler{hdlr}
return s.Handle(s.NewHandler(&Streams{h}, opts...))
}
type streamsHandler struct {
StreamsHandler
}
func (h *streamsHandler) Publish(ctx context.Context, in *Message, out *PublishResponse) error {
return h.StreamsHandler.Publish(ctx, in, out)
}
func (h *streamsHandler) Subscribe(ctx context.Context, stream server.Stream) error {
m := new(SubscribeRequest)
if err := stream.Recv(m); err != nil {
return err
}
return h.StreamsHandler.Subscribe(ctx, m, &streamsSubscribeStream{stream})
}
type Streams_SubscribeStream interface {
Context() context.Context
SendMsg(interface{}) error
RecvMsg(interface{}) error
Close() error
Send(*Message) error
}
type streamsSubscribeStream struct {
stream server.Stream
}
func (x *streamsSubscribeStream) Close() error {
return x.stream.Close()
}
func (x *streamsSubscribeStream) Context() context.Context {
return x.stream.Context()
}
func (x *streamsSubscribeStream) SendMsg(m interface{}) error {
return x.stream.Send(m)
}
func (x *streamsSubscribeStream) RecvMsg(m interface{}) error {
return x.stream.Recv(m)
}
func (x *streamsSubscribeStream) Send(m *Message) error {
return x.stream.Send(m)
}
func (h *streamsHandler) Token(ctx context.Context, in *TokenRequest, out *TokenResponse) error {
return h.StreamsHandler.Token(ctx, in, out)
}

View File

@@ -1,38 +0,0 @@
syntax = "proto3";
package streams;
option go_package = "./proto;streams";
import "google/protobuf/timestamp.proto";
service Streams {
rpc Publish(Message) returns (PublishResponse) {}
rpc Subscribe(SubscribeRequest) returns (stream Message) {}
rpc Token(TokenRequest) returns (TokenResponse) {}
}
message PublishResponse {}
message Message {
string topic = 1;
string message = 2;
google.protobuf.Timestamp sent_at = 3;
}
message SubscribeRequest {
// topic the user wishes to subscribe to, required
string topic = 1;
// tokens should be provided if the user is not proving an API key on the request (e.g. in cases
// where the stream is being consumed directly from the frontend via websockets). tokens can be
// generated using the Token RPC
string token = 2;
}
message TokenRequest {
// the topic the token should be restricted to, if no topic is required the token can be used to
// subscribe to any topic
string topic = 1;
}
message TokenResponse {
string token = 1;
}

2
threads/.gitignore vendored
View File

@@ -1,2 +0,0 @@
streams

View File

@@ -1,3 +0,0 @@
FROM alpine
ADD threads /threads
ENTRYPOINT [ "/threads" ]

View File

@@ -1,27 +0,0 @@
GOPATH:=$(shell go env GOPATH)
.PHONY: init
init:
go get -u github.com/golang/protobuf/proto
go get -u github.com/golang/protobuf/protoc-gen-go
go get github.com/micro/micro/v3/cmd/protoc-gen-micro
.PHONY: proto
proto:
protoc --proto_path=. --micro_out=. --go_out=:. proto/threads.proto
.PHONY: docs
docs:
protoc --openapi_out=. --proto_path=. --micro_out=. --go_out=:. proto/threads.proto
@redoc-cli bundle api-threads.json
.PHONY: build
build:
go build -o threads *.go
.PHONY: test
test:
go test -v ./... -cover
.PHONY: docker
docker:
docker build . -t threads:latest

View File

@@ -1,6 +0,0 @@
Threaded conversations
# Threads Service
Threads provides threaded conversations as a service grouped by topics.

View File

@@ -1,3 +0,0 @@
package main
//go:generate make proto

View File

@@ -1,74 +0,0 @@
package handler
import (
"context"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/services/pkg/model"
pb "github.com/micro/services/threads/proto"
)
// Create a message within a thread
func (s *Threads) CreateMessage(ctx context.Context, req *pb.CreateMessageRequest, rsp *pb.CreateMessageResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.AuthorId) == 0 {
return ErrMissingAuthorID
}
if len(req.ThreadId) == 0 {
return ErrMissingThreadID
}
if len(req.Text) == 0 {
return ErrMissingText
}
// lookup the thread
conv := Thread{ID: req.ThreadId}
if err := model.Read(ctx, &conv); err == model.ErrNotFound {
return ErrNotFound
} else if err != nil {
logger.Errorf("Error reading thread: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to database")
}
// create the message
msg := &Message{
ID: req.Id,
SentAt: s.Time(),
Text: req.Text,
AuthorID: req.AuthorId,
ThreadID: req.ThreadId,
}
if len(msg.ID) == 0 {
msg.ID = uuid.New().String()
}
if err := model.Create(ctx, msg); err == nil {
rsp.Message = msg.Serialize()
return nil
} else if err != model.ErrAlreadyExists {
logger.Errorf("Error creating message: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to database")
}
// a message already exists with this id
existing := &Message{ID: msg.ID, ThreadID: req.ThreadId}
if err := model.Read(ctx, existing); err == model.ErrNotFound {
return ErrNotFound
} else if err != nil {
logger.Errorf("Error creating message: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to database")
}
// return the message
rsp.Message = existing.Serialize()
return nil
}

View File

@@ -1,107 +0,0 @@
package handler_test
import (
"testing"
"github.com/micro/services/threads/handler"
pb "github.com/micro/services/threads/proto"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestCreateMessage(t *testing.T) {
h := testHandler(t)
// seed some data
var cRsp pb.CreateThreadResponse
err := h.CreateThread(microAccountCtx(), &pb.CreateThreadRequest{
Topic: "HelloWorld", GroupId: uuid.New().String(),
}, &cRsp)
if err != nil {
t.Fatalf("Error creating thread: %v", err)
return
}
iid := uuid.New().String()
tt := []struct {
Name string
AuthorID string
ThreadID string
ID string
Text string
Error error
}{
{
Name: "MissingThreadID",
Text: "HelloWorld",
AuthorID: uuid.New().String(),
Error: handler.ErrMissingThreadID,
},
{
Name: "MissingAuthorID",
ThreadID: uuid.New().String(),
Text: "HelloWorld",
Error: handler.ErrMissingAuthorID,
},
{
Name: "MissingText",
ThreadID: uuid.New().String(),
AuthorID: uuid.New().String(),
Error: handler.ErrMissingText,
},
{
Name: "ThreadNotFound",
ThreadID: uuid.New().String(),
AuthorID: uuid.New().String(),
Text: "HelloWorld",
Error: handler.ErrNotFound,
},
{
Name: "NoID",
ThreadID: cRsp.Thread.Id,
AuthorID: uuid.New().String(),
Text: "HelloWorld",
},
{
Name: "WithID",
ThreadID: cRsp.Thread.Id,
Text: "HelloWorld",
AuthorID: "johndoe",
ID: iid,
},
{
Name: "RepeatID",
ThreadID: cRsp.Thread.Id,
Text: "HelloWorld",
AuthorID: "johndoe",
ID: iid,
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
var rsp pb.CreateMessageResponse
err := h.CreateMessage(microAccountCtx(), &pb.CreateMessageRequest{
AuthorId: tc.AuthorID,
ThreadId: tc.ThreadID,
Text: tc.Text,
Id: tc.ID,
}, &rsp)
assert.Equal(t, tc.Error, err)
if tc.Error != nil {
assert.Nil(t, rsp.Message)
return
}
assertMessagesMatch(t, &pb.Message{
Id: tc.ID,
AuthorId: tc.AuthorID,
ThreadId: tc.ThreadID,
SentAt: handler.FormatTime(h.Time()),
Text: tc.Text,
}, rsp.Message)
})
}
}

View File

@@ -1,45 +0,0 @@
package handler
import (
"context"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/services/pkg/model"
pb "github.com/micro/services/threads/proto"
)
// Create a thread
func (s *Threads) CreateThread(ctx context.Context, req *pb.CreateThreadRequest, rsp *pb.CreateThreadResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.GroupId) == 0 {
return ErrMissingGroupID
}
if len(req.Topic) == 0 {
return ErrMissingTopic
}
// write the thread to the database
thread := &Thread{
ID: uuid.New().String(),
Topic: req.Topic,
GroupID: req.GroupId,
CreatedAt: s.Time(),
}
// write the thread to the database
if err := model.Create(ctx, thread); err != nil {
logger.Errorf("Error creating thread: %v", err)
return err
}
// serialize the response
rsp.Thread = thread.Serialize()
return nil
}

View File

@@ -1,58 +0,0 @@
package handler_test
import (
"testing"
"github.com/micro/services/threads/handler"
pb "github.com/micro/services/threads/proto"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestCreateThread(t *testing.T) {
tt := []struct {
Name string
GroupID string
Topic string
Error error
}{
{
Name: "MissingGroupID",
Topic: "HelloWorld",
Error: handler.ErrMissingGroupID,
},
{
Name: "MissingTopic",
GroupID: uuid.New().String(),
Error: handler.ErrMissingTopic,
},
{
Name: "Valid",
GroupID: uuid.New().String(),
Topic: "HelloWorld",
},
}
h := testHandler(t)
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
var rsp pb.CreateThreadResponse
err := h.CreateThread(microAccountCtx(), &pb.CreateThreadRequest{
Topic: tc.Topic, GroupId: tc.GroupID,
}, &rsp)
assert.Equal(t, tc.Error, err)
if tc.Error != nil {
assert.Nil(t, rsp.Thread)
return
}
assertThreadsMatch(t, &pb.Thread{
CreatedAt: handler.FormatTime(h.Time()),
GroupId: tc.GroupID,
Topic: tc.Topic,
}, rsp.Thread)
})
}
}

View File

@@ -1,41 +0,0 @@
package handler
import (
"context"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
"github.com/micro/services/pkg/model"
pb "github.com/micro/services/threads/proto"
)
// Delete a thread and all the messages within
func (s *Threads) DeleteThread(ctx context.Context, req *pb.DeleteThreadRequest, rsp *pb.DeleteThreadResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.Id) == 0 {
return ErrMissingID
}
thread := Thread{ID: req.Id}
// delete the thread
if err := model.Delete(ctx, &thread); err != nil {
logger.Errorf("Error deleting thread: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to database")
}
message := Message{ThreadID: req.Id}
// delete the messages
if err := model.Delete(ctx, &message); err != nil {
logger.Errorf("Error deleting messages: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to database")
}
return nil
}

View File

@@ -1,49 +0,0 @@
package handler_test
import (
"testing"
"github.com/micro/services/threads/handler"
pb "github.com/micro/services/threads/proto"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestDeleteThread(t *testing.T) {
h := testHandler(t)
// seed some data
var cRsp pb.CreateThreadResponse
err := h.CreateThread(microAccountCtx(), &pb.CreateThreadRequest{
Topic: "HelloWorld", GroupId: uuid.New().String(),
}, &cRsp)
if err != nil {
t.Fatalf("Error creating thread: %v", err)
return
}
t.Run("MissingID", func(t *testing.T) {
err := h.DeleteThread(microAccountCtx(), &pb.DeleteThreadRequest{}, &pb.DeleteThreadResponse{})
assert.Equal(t, handler.ErrMissingID, err)
})
t.Run("Valid", func(t *testing.T) {
err := h.DeleteThread(microAccountCtx(), &pb.DeleteThreadRequest{
Id: cRsp.Thread.Id,
}, &pb.DeleteThreadResponse{})
assert.NoError(t, err)
err = h.ReadThread(microAccountCtx(), &pb.ReadThreadRequest{
Id: cRsp.Thread.Id,
}, &pb.ReadThreadResponse{})
assert.Equal(t, handler.ErrNotFound, err)
})
t.Run("Retry", func(t *testing.T) {
err := h.DeleteThread(microAccountCtx(), &pb.DeleteThreadRequest{
Id: cRsp.Thread.Id,
}, &pb.DeleteThreadResponse{})
assert.NoError(t, err)
})
}

Some files were not shown because too many files have changed in this diff Show More