add service layer on top of store

This commit is contained in:
Umputun
2017-12-27 01:11:01 -06:00
parent d0acfc5069
commit ca9afd2c8b
8 changed files with 141 additions and 122 deletions
+2 -1
View File
@@ -72,12 +72,13 @@ func main() {
return
}
dataService := store.Service{Interface: dataStore}
sessionStore := sessions.NewFilesystemStore(opts.ServerCommand.SessionStore, []byte(opts.ServerCommand.StoreKey))
exporter := migrator.Remark{DataStore: dataStore}
srv := rest.Server{
Version: revision,
Store: dataStore,
DataService: dataService,
SessionStore: sessionStore,
Admins: opts.Admins,
DevMode: opts.DevMode,
+7 -7
View File
@@ -15,9 +15,9 @@ import (
// admin provides router for all requests available for admin only
type admin struct {
dataStore store.Interface
exporter migrator.Exporter
respCache *cache.Cache
dataService store.Service
exporter migrator.Exporter
respCache *cache.Cache
}
func (a *admin) routes() chi.Router {
@@ -37,7 +37,7 @@ func (a *admin) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) {
log.Printf("[INFO] delete comment %s", id)
url := r.URL.Query().Get("url")
err := a.dataStore.Delete(store.Locator{URL: url}, id)
err := a.dataService.Delete(store.Locator{URL: url}, id)
if err != nil {
log.Printf("[WARN] can't delete comment, %s", err)
httpError(w, r, http.StatusInternalServerError, err, "can't delete comment")
@@ -54,7 +54,7 @@ func (a *admin) setBlockCtrl(w http.ResponseWriter, r *http.Request) {
siteID := r.URL.Query().Get("site")
blockStatus := r.URL.Query().Get("block") == "1"
if err := a.dataStore.SetBlock(store.Locator{SiteID: siteID}, userID, blockStatus); err != nil {
if err := a.dataService.SetBlock(store.Locator{SiteID: siteID}, userID, blockStatus); err != nil {
httpError(w, r, http.StatusBadRequest, err, "can't set blocking status")
return
}
@@ -68,7 +68,7 @@ func (a *admin) setPinCtrl(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("url")
pinStatus := r.URL.Query().Get("pin") == "1"
if err := a.dataStore.SetPin(store.Locator{URL: url}, commentID, pinStatus); err != nil {
if err := a.dataService.SetPin(store.Locator{URL: url}, commentID, pinStatus); err != nil {
httpError(w, r, http.StatusBadRequest, err, "can't set pin status")
return
}
@@ -84,5 +84,5 @@ func (a *admin) exportCtrl(w http.ResponseWriter, r *http.Request) {
}
}
func (a *admin) checkBlocked(locator store.Locator, user store.User) bool {
return a.dataStore.IsBlocked(store.Locator{}, user.ID)
return a.dataService.IsBlocked(store.Locator{}, user.ID)
}
+10 -10
View File
@@ -27,7 +27,7 @@ import (
// Server is a rest access server
type Server struct {
Version string
Store store.Interface
DataService store.Service
Admins []string
AuthGoogle *auth.Provider
AuthGithub *auth.Provider
@@ -80,7 +80,7 @@ func (s *Server) Run() {
rauth.Get("/user", s.userInfoCtrl)
rauth.Put("/vote/{id}", s.voteCtrl)
s.mod = admin{dataStore: s.Store, exporter: s.Exporter, respCache: s.respCache}
s.mod = admin{dataService: s.DataService, exporter: s.Exporter, respCache: s.respCache}
rauth.Mount("/admin", s.mod.routes())
})
@@ -140,7 +140,7 @@ func (s *Server) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
return
}
id, err := s.Store.Create(comment)
id, err := s.DataService.Create(comment)
if err != nil {
log.Printf("[WARN] can't save comment, %s", err)
httpError(w, r, http.StatusInternalServerError, err, "can't save comment")
@@ -160,7 +160,7 @@ func (s *Server) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) {
log.Printf("[DEBUG] delete comment %s", id)
url := r.URL.Query().Get("url")
err := s.Store.Delete(store.Locator{URL: url}, id)
err := s.DataService.Delete(store.Locator{URL: url}, id)
if err != nil {
log.Printf("[WARN] can't delete comment, %s", err)
httpError(w, r, http.StatusInternalServerError, err, "can't delete comment")
@@ -185,7 +185,7 @@ func (s *Server) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
return
}
comments, err := s.Store.Find(store.Request{Locator: store.Locator{URL: url}, Sort: r.URL.Query().Get("sort")})
comments, err := s.DataService.Find(store.Request{Locator: store.Locator{URL: url}, Sort: r.URL.Query().Get("sort")})
if err != nil {
log.Printf("[WARN] can't get comments for %s, %s", url, err)
httpError(w, r, http.StatusInternalServerError, err, "can't load comments comment")
@@ -216,7 +216,7 @@ func (s *Server) lastCommentsCtrl(w http.ResponseWriter, r *http.Request) {
return
}
comments, err := s.Store.Last(store.Locator{}, max)
comments, err := s.DataService.Last(store.Locator{}, max)
if err != nil {
log.Printf("[WARN] can't get last comments, %s", err)
httpError(w, r, http.StatusInternalServerError, err, "can't get last comments")
@@ -236,7 +236,7 @@ func (s *Server) commentByIDCtrl(w http.ResponseWriter, r *http.Request) {
log.Printf("[DEBUG] get comments by id %s, %s", id, url)
comment, err := s.Store.Get(store.Locator{URL: url}, id)
comment, err := s.DataService.GetByID(store.Locator{URL: url}, id)
if err != nil {
log.Printf("[WARN] can't get comment, %s", err)
httpError(w, r, http.StatusInternalServerError, err, "can't get comment by id")
@@ -260,7 +260,7 @@ func (s *Server) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) {
return
}
comments, err := s.Store.GetForUser(store.Locator{}, userID)
comments, err := s.DataService.GetByUser(store.Locator{}, userID)
if err != nil {
log.Printf("[WARN] can't get comment, %s", err)
httpError(w, r, http.StatusBadRequest, err, "can't get comment by user id")
@@ -285,7 +285,7 @@ func (s *Server) userInfoCtrl(w http.ResponseWriter, r *http.Request) {
// GET /count?url=post-url
func (s *Server) countCtrl(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("url")
count, err := s.Store.Count(store.Locator{URL: url})
count, err := s.DataService.Count(store.Locator{URL: url})
if err != nil {
httpError(w, r, http.StatusBadRequest, err, "can't get count")
return
@@ -308,7 +308,7 @@ func (s *Server) voteCtrl(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("url")
vote := r.URL.Query().Get("vote") == "1"
comment, err := s.Store.Vote(store.Locator{URL: url}, id, user.ID, vote)
comment, err := s.DataService.Vote(store.Locator{URL: url}, id, user.ID, vote)
if err != nil {
log.Printf("[WARN] vote rejected for %s - %s, %s", user.ID, id, err)
httpError(w, r, http.StatusBadRequest, err, "can't vote for comment")
+16 -47
View File
@@ -178,8 +178,8 @@ func (b *BoltDB) Find(request Request) ([]Comment, error) {
return res, err
}
// Get comment by id across posts
func (b *BoltDB) Get(locator Locator, commentID string) (comment Comment, err error) {
// GetByID returns comment by id across posts
func (b *BoltDB) GetByID(locator Locator, commentID string) (comment Comment, err error) {
err = b.View(func(tx *bolt.Tx) error {
@@ -262,29 +262,6 @@ func (b *BoltDB) Last(locator Locator, max int) (result []Comment, err error) {
return result, err
}
// Vote for comment by id and locator
func (b *BoltDB) Vote(locator Locator, commentID string, userID string, val bool) (comment Comment, err error) {
comment, err = b.getComment(locator.URL, commentID)
if err != nil {
return comment, err
}
if _, voted := comment.Votes[userID]; voted {
return comment, errors.Errorf("user %s already voted for %s", userID, commentID)
}
// update votes and score
comment.Votes[userID] = val
if val {
comment.Score++
} else {
comment.Score--
}
return comment, b.putComment(locator.URL, comment)
}
// Count returns number of comments for locator
func (b *BoltDB) Count(locator Locator) (count int, err error) {
err = b.View(func(tx *bolt.Tx) error {
@@ -348,19 +325,9 @@ func (b BoltDB) List(locator Locator) (result []string, err error) {
return result, err
}
// SetPin pin/un-pin comment as special
func (b *BoltDB) SetPin(locator Locator, commentID string, status bool) error {
comment, err := b.getComment(locator.URL, commentID)
if err != nil {
return err
}
comment.Pin = status
return b.putComment(locator.URL, comment)
}
// GetForUser extracts all comments for given site and given userID
// GetByUser extracts all comments for given site and given userID
// "users" bucket has sub-bucket for each userID, and keeps it as ts:ref
func (b *BoltDB) GetForUser(locator Locator, userID string) (comments []Comment, err error) {
func (b *BoltDB) GetByUser(locator Locator, userID string) (comments []Comment, err error) {
comments = []Comment{}
commentRefs := []string{}
@@ -397,7 +364,7 @@ func (b *BoltDB) GetForUser(locator Locator, userID string) (comments []Comment,
if e != nil {
return comments, errors.Wrapf(e, "can't parse reference %s", v)
}
if c, e := b.Get(Locator{URL: url, SiteID: locator.SiteID}, commentID); e == nil {
if c, e := b.GetByID(Locator{URL: url, SiteID: locator.SiteID}, commentID); e == nil {
comments = append(comments, c)
}
}
@@ -405,18 +372,19 @@ func (b *BoltDB) GetForUser(locator Locator, userID string) (comments []Comment,
return comments, err
}
func (b *BoltDB) getComment(url string, commentID string) (comment Comment, err error) {
// GetComment for locator.URL and commentID string
func (b *BoltDB) GetComment(locator Locator, commentID string) (comment Comment, err error) {
err = b.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(url))
bucket := tx.Bucket([]byte(locator.URL))
if bucket == nil {
return errors.Errorf("no bucket %s in store", url)
return errors.Errorf("no bucket %s in store", locator.URL)
}
// get and unmarshal comment
commentVal := bucket.Get([]byte(commentID))
if commentVal == nil {
return errors.Errorf("no comment for %s in store %s", commentID, url)
return errors.Errorf("no comment for %s in store %s", commentID, locator.URL)
}
if e := json.Unmarshal(commentVal, &comment); e != nil {
@@ -427,9 +395,10 @@ func (b *BoltDB) getComment(url string, commentID string) (comment Comment, err
return comment, err
}
func (b *BoltDB) putComment(url string, comment Comment) error {
// PutComment updates comment for locator.URL with mutable part of comment
func (b *BoltDB) PutComment(locator Locator, comment Comment) error {
if curComment, err := b.getComment(url, comment.ID); err == nil {
if curComment, err := b.GetComment(locator, comment.ID); err == nil {
// preserve immutable fields
comment.ParentID = curComment.ParentID
comment.Locator = curComment.Locator
@@ -438,9 +407,9 @@ func (b *BoltDB) putComment(url string, comment Comment) error {
}
return b.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(url))
bucket := tx.Bucket([]byte(locator.URL))
if bucket == nil {
return errors.Errorf("no bucket %s in store", url)
return errors.Errorf("no bucket %s in store", locator.URL)
}
// serialize comment to json []byte for bolt and save
@@ -449,7 +418,7 @@ func (b *BoltDB) putComment(url string, comment Comment) error {
return errors.Wrap(jerr, "can't marshal comment")
}
if err := bucket.Put([]byte(comment.ID), jdata); err != nil {
return errors.Wrapf(err, "failed to put key %s to bucket %s", comment.ID, url)
return errors.Wrapf(err, "failed to put key %s to bucket %s", comment.ID, locator.URL)
}
return nil
})
+4 -53
View File
@@ -45,7 +45,7 @@ func TestBoltDB_Delete(t *testing.T) {
assert.Equal(t, 1, len(comments), "only 1 left in last")
}
func TestBoltDB_Get(t *testing.T) {
func TestBoltDB_GetByID(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
@@ -53,11 +53,11 @@ func TestBoltDB_Get(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
comment, err := b.Get(Locator{URL: "https://radio-t.com"}, res[1].ID)
comment, err := b.GetByID(Locator{URL: "https://radio-t.com"}, res[1].ID)
assert.Nil(t, err)
assert.Equal(t, "some text2", comment.Text)
comment, err = b.Get(Locator{URL: "https://radio-t.com"}, "1234567")
comment, err = b.GetByID(Locator{URL: "https://radio-t.com"}, "1234567")
assert.NotNil(t, err)
}
@@ -76,31 +76,6 @@ func TestBoltDB_Last(t *testing.T) {
assert.Equal(t, "some text2", res[0].Text)
}
func TestBoltDB_Vote(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
res, err := b.Last(Locator{URL: "https://radio-t.com"}, 0)
t.Logf("%+v", res[0])
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
assert.Equal(t, 0, res[0].Score)
assert.Equal(t, map[string]bool{}, res[0].Votes)
c, err := b.Vote(Locator{URL: "https://radio-t.com"}, res[0].ID, "user1", true)
assert.Nil(t, err)
assert.Equal(t, 1, c.Score)
assert.Equal(t, map[string]bool{"user1": true}, c.Votes)
_, err = b.Vote(Locator{URL: "https://radio-t.com"}, res[0].ID, "user1", true)
assert.NotNil(t, err, "double-voting rejected")
res, err = b.Last(Locator{URL: "https://radio-t.com"}, 0)
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
assert.Equal(t, 1, res[0].Score)
}
func TestBoltDB_Count(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
@@ -141,35 +116,11 @@ func TestBoltDB_List(t *testing.T) {
assert.Equal(t, []string{"https://radio-t.com", "https://radio-t.com/2"}, res)
}
func TestBoltDB_Pin(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
res, err := b.Last(Locator{URL: "https://radio-t.com"}, 0)
t.Logf("%+v", res[0])
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
assert.Equal(t, false, res[0].Pin)
err = b.SetPin(Locator{URL: "https://radio-t.com"}, res[0].ID, true)
assert.Nil(t, err)
c, err := b.Get(Locator{URL: "https://radio-t.com"}, res[0].ID)
assert.Nil(t, err)
assert.Equal(t, true, c.Pin)
err = b.SetPin(Locator{URL: "https://radio-t.com"}, res[0].ID, false)
assert.Nil(t, err)
c, err = b.Get(Locator{URL: "https://radio-t.com"}, res[0].ID)
assert.Nil(t, err)
assert.Equal(t, false, c.Pin)
}
func TestBoltDB_GetForUser(t *testing.T) {
defer os.Remove(testDb)
b := prep(t)
res, err := b.GetForUser(Locator{SiteID: "radio-t"}, "user1")
res, err := b.GetByUser(Locator{SiteID: "radio-t"}, "user1")
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
assert.Equal(t, "some text2", res[0].Text, "sorted by -time")
+41
View File
@@ -0,0 +1,41 @@
package store
import "github.com/pkg/errors"
// Service wraps store.Interface with additional methods
type Service struct {
Interface
}
// SetPin pin/un-pin comment as special
func (s *Service) SetPin(locator Locator, commentID string, status bool) error {
comment, err := s.GetComment(locator, commentID)
if err != nil {
return err
}
comment.Pin = status
return s.PutComment(locator, comment)
}
// Vote for comment by id and locator
func (s *Service) Vote(locator Locator, commentID string, userID string, val bool) (comment Comment, err error) {
comment, err = s.GetComment(locator, commentID)
if err != nil {
return comment, err
}
if _, voted := comment.Votes[userID]; voted {
return comment, errors.Errorf("user %s already voted for %s", userID, commentID)
}
// update votes and score
comment.Votes[userID] = val
if val {
comment.Score++
} else {
comment.Score--
}
return comment, s.PutComment(locator, comment)
}
+57
View File
@@ -0,0 +1,57 @@
package store
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestService_Vote(t *testing.T) {
defer os.Remove(testDb)
b := Service{Interface: prep(t)}
res, err := b.Last(Locator{URL: "https://radio-t.com"}, 0)
t.Logf("%+v", res[0])
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
assert.Equal(t, 0, res[0].Score)
assert.Equal(t, map[string]bool{}, res[0].Votes)
c, err := b.Vote(Locator{URL: "https://radio-t.com"}, res[0].ID, "user1", true)
assert.Nil(t, err)
assert.Equal(t, 1, c.Score)
assert.Equal(t, map[string]bool{"user1": true}, c.Votes)
_, err = b.Vote(Locator{URL: "https://radio-t.com"}, res[0].ID, "user1", true)
assert.NotNil(t, err, "double-voting rejected")
res, err = b.Last(Locator{URL: "https://radio-t.com"}, 0)
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
assert.Equal(t, 1, res[0].Score)
}
func TestBoltDB_Pin(t *testing.T) {
defer os.Remove(testDb)
b := Service{Interface: prep(t)}
res, err := b.Last(Locator{URL: "https://radio-t.com"}, 0)
t.Logf("%+v", res[0])
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
assert.Equal(t, false, res[0].Pin)
err = b.SetPin(Locator{URL: "https://radio-t.com"}, res[0].ID, true)
assert.Nil(t, err)
c, err := b.GetByID(Locator{URL: "https://radio-t.com"}, res[0].ID)
assert.Nil(t, err)
assert.Equal(t, true, c.Pin)
err = b.SetPin(Locator{URL: "https://radio-t.com"}, res[0].ID, false)
assert.Nil(t, err)
c, err = b.GetByID(Locator{URL: "https://radio-t.com"}, res[0].ID)
assert.Nil(t, err)
assert.Equal(t, false, c.Pin)
}
+4 -4
View File
@@ -54,18 +54,18 @@ type Request struct {
// Interface defines basic CRUD for comments
type Interface interface {
Create(comment Comment) (commentID string, err error)
GetComment(locator Locator, commentID string) (comment Comment, err error)
PutComment(locator Locator, comment Comment) error
Delete(locator Locator, commentID string) error
Find(request Request) ([]Comment, error)
Last(locator Locator, max int) ([]Comment, error)
Get(locator Locator, commentID string) (Comment, error)
Vote(locator Locator, commentID string, userID string, val bool) (Comment, error)
GetByID(locator Locator, commentID string) (Comment, error)
GetByUser(locator Locator, userID string) ([]Comment, error)
Count(locator Locator) (int, error)
List(locator Locator) ([]string, error)
GetForUser(locator Locator, userID string) ([]Comment, error)
SetBlock(locator Locator, userID string, status bool) error
IsBlocked(locator Locator, userID string) bool
SetPin(locator Locator, commentID string, status bool) error
}
func makeCommentID() string {