Browse Source

bleve intergration ongoing

master
Bernd-René Predota 4 years ago
parent
commit
392e7e1af3
  1. 2
      api/v1/tokens/tokens.go
  2. 2
      api/v1/users/route.go
  3. 129
      api/v1/users/users.go
  4. 48
      api/v1/users/users_test.go
  5. 2
      cmd/add/admin/admin.go
  6. 28
      cmd/server/server.go
  7. 3
      go.mod
  8. 92
      go.sum
  9. 62
      indexes/indexes.go
  10. 168
      lib/apperrors/apperrors.go
  11. 41
      lib/apperrors/constants.go
  12. 5
      lib/authentication/authentication.go
  13. 45
      lib/response/response.go
  14. 13
      lib/response/response_test.go
  15. 3
      main.go
  16. 37
      repositories/token_test.go
  17. 46
      repositories/user.go
  18. 41
      repositories/user_test.go
  19. 35
      services/token_test.go
  20. 27
      services/user.go
  21. 37
      services/user_test.go

2
api/v1/tokens/tokens.go

@ -21,7 +21,7 @@ func Create(c *gin.Context) {
// }
tokenRepo := repositories.NewTokenRepository(db, "01F5FSJXDHWT4HK93B9NB8V5G4", "test", cc)
tokenService := services.NewTokenService(tokenRepo)
userID, _ := common.StringToGUID("01F5G1W6WJCKQ4PPRS36RYGQ08")
userID, _ := common.StringToGUID("01F67CNK67MVHFHPK73R7RR0A2")
t := &models.Token{
ExpiresAt: time.Now().Add(24 * time.Hour),
UserID: userID,

2
api/v1/users/route.go

@ -9,7 +9,7 @@ import (
func ApplyRoutes(r *gin.RouterGroup) *gin.RouterGroup {
users := r.Group("/users")
{
// actions.POST("", middlewares.Authorized, CreateActions)
users.POST("", authentication.Authorized, Create)
users.GET("/:username", authentication.Authorized, ReadByID)
users.GET("", authentication.Authorized, ReadAll)
// actions.GET("/:id/logs", middlewares.Authorized, actionlogs.ReadActionLogs)

129
api/v1/users/users.go

@ -1,11 +1,21 @@
package users
import (
"fmt"
"net/http"
"strconv"
"git.devices.local/mawas/golang-api-skeleton/lib/apperrors"
"git.devices.local/mawas/golang-api-skeleton/lib/cache"
"git.devices.local/mawas/golang-api-skeleton/lib/common"
"git.devices.local/mawas/golang-api-skeleton/lib/response"
"git.devices.local/mawas/golang-api-skeleton/models"
"git.devices.local/mawas/golang-api-skeleton/repositories"
"git.devices.local/mawas/golang-api-skeleton/services"
"github.com/blevesearch/bleve"
"github.com/blevesearch/bleve/v2/search"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"gorm.io/gorm"
)
@ -23,23 +33,134 @@ func prepare(c *gin.Context) (response.Envelope, *services.UserService) {
return resp, userService
}
// Create handler function for POST /api/v1/users
func Create(c *gin.Context) {
var requestBody []*models.User
resp, userService := prepare(c)
defer resp.Recovery(c)
if err := c.ShouldBindBodyWith(&requestBody, binding.JSON); err != nil {
c.AbortWithStatusJSON(resp.HTTPError(apperrors.NewError(
apperrors.ValidationFailed,
err.Error(),
)))
return
}
users, err := userService.Create(requestBody)
if err != nil {
c.AbortWithStatusJSON(resp.HTTPError(err))
return
}
index := c.MustGet("index").(bleve.Index)
for i := range users {
index.Index(users[i].ID.String(), users[i])
}
c.JSON(http.StatusOK, resp.SetSuccess(users))
}
// ReadByID handler function for path GET /api/v1/users/:id
func ReadByID(c *gin.Context) {
resp, userService := prepare(c)
username := c.Param("username")
// index, err := bleve.Open("users.bleve")
// if err != nil {
// fmt.Println("error", err.Error())
// }
index := c.MustGet("index").(bleve.Index)
raw, err := index.Document(username)
if err != nil {
fmt.Println("error", err.Error())
}
fmt.Println("<>>>>>>", string(raw.Fields[0].Value()))
query := bleve.NewMatchQuery("jdoe")
search := bleve.NewSearchRequest(query)
searchResults, err := index.Search(search)
if err != nil {
fmt.Println("error", err.Error())
}
fmt.Println(searchResults)
user, err := userService.ReadByID(username)
if err != nil {
c.AbortWithStatusJSON(500, resp.AppendError(err))
c.AbortWithStatusJSON(resp.HTTPError(err))
return
}
c.JSON(200, resp.SetSuccess(user))
c.JSON(http.StatusOK, resp.SetSuccess(user))
}
// ReadALL handler function for path GET /api/v1/users
func ReadAll(c *gin.Context) {
filter := c.Query("filter")
page, _ := strconv.Atoi(c.Query("page"))
limit, _ := strconv.Atoi(c.Query("limit"))
if page > 0 {
page += -1
}
fmt.Println(page * limit)
resp, userService := prepare(c)
// filter := "tokens:01F5G2F9B2CZZVV67MVDMJ5SBZ"
// filter := "firstname:jane"
// filter := "*mustermann*"
index := c.MustGet("index").(bleve.Index)
// query := bleve.NewMatchQuery(filter)
var err error
if filter != "" {
query := bleve.NewQueryStringQuery(filter)
searchRequest := bleve.NewSearchRequest(query)
s := search.SortOrder{
&search.SortField{
Field: "Username",
Missing: search.SortFieldMissingFirst,
},
&search.SortDocID{},
}
searchRequest.SortByCustom(s)
// search.Fields = []string{"Username", "Firstname", "Lastname", "Email", "Tokens"}
searchRequest.SortBy([]string{"_id"})
searchRequest.IncludeLocations = true
searchResults, err := index.Search(searchRequest)
if err != nil {
fmt.Println("error", err.Error())
}
ids := make([]common.GUID, len(searchResults.Hits))
for i := range searchResults.Hits {
ids[i], _ = common.StringToGUID(searchResults.Hits[i].ID)
}
debugFilterPrint(searchResults)
users, err := userService.ReadSelection(ids)
if err != nil {
c.AbortWithStatusJSON(resp.HTTPError(err))
return
}
c.JSON(http.StatusOK, resp.SetSuccess(users))
return
}
// fmt.Println("Limit:", limit, "Page:", page)
// search := bleve.NewSearchRequest(query)
// if limit > 0 {
// search = bleve.NewSearchRequestOptions(query, limit, page*limit, true)
// }
users, err := userService.ReadAll()
if err != nil {
c.AbortWithStatusJSON(500, resp.AppendError(err))
c.AbortWithStatusJSON(resp.HTTPError(err))
return
}
c.JSON(200, resp.SetSuccess(users))
c.JSON(http.StatusOK, resp.SetSuccess(users))
}
func debugFilterPrint(searchResults *bleve.SearchResult) {
fmt.Println("Total:", searchResults.Total, len(searchResults.Hits))
for _, hit := range searchResults.Hits {
fmt.Println(hit.Fields)
for k, v := range hit.Fields {
fmt.Printf("Field %v = %v\n", k, v)
}
for fieldName, fieldMap := range hit.Locations {
for termName, locations := range fieldMap {
for _, location := range locations {
// may be usefull to report back for highlighting in filter_info or so
fmt.Printf(" Field %s has term %s from %d to %d (Pos %d?)\n", fieldName, termName, location.Start, location.End, location.Pos)
}
}
}
}
}

48
api/v1/users/users_test.go

@ -0,0 +1,48 @@
package users
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func usersMock(t *testing.T) *gin.Engine {
gin.SetMode(gin.TestMode)
app := gin.Default()
// create token
return app
}
func TestUsersEndpoint(t *testing.T) {
app := usersMock(t)
api := app.Group("/api/v1")
ApplyRoutes(api)
request, err := http.NewRequest("GET", "/api/v1/users", nil)
if err != nil {
t.Error(err)
}
// Unauthorized
response := httptest.NewRecorder()
app.ServeHTTP(response, request)
if response.Result().StatusCode != http.StatusUnauthorized {
t.Errorf("api users - http status code must be \"%d\" (but is \"%d\") because of unauthorized request", http.StatusUnauthorized, response.Result().StatusCode)
}
body := response.Body.String()
var j map[string]interface{}
if err := json.Unmarshal([]byte(body), &j); err != nil {
t.Error(err)
}
if _, exists := j["success"]; !exists {
t.Error("api users - success flag needs always be given")
} else if success, ok := j["success"].(bool); !ok {
t.Error("api users - success flag needs to be boolean")
} else if success {
t.Error("api users - success flag needs to be false because of unauthorized request")
}
// Create user
// request.Header.Set("Authorization", "Bearer "+token)
// t.Error(response)
}

2
cmd/add/admin/admin.go

@ -68,6 +68,6 @@ func createAdminUser(appName, cfgFile string, flagCfg map[string]interface{}, us
userService := services.NewUserService(userRepo)
adminUser := services.NewUser(username, firstname, lastname, email)
adminUser.Active = true
_, err = userService.Create(&adminUser)
_, err = userService.Create([]*models.User{&adminUser})
return err
}

28
cmd/server/server.go

@ -4,6 +4,7 @@ import (
"fmt"
"git.devices.local/mawas/golang-api-skeleton/api"
"git.devices.local/mawas/golang-api-skeleton/indexes"
"git.devices.local/mawas/golang-api-skeleton/lib/cache"
"git.devices.local/mawas/golang-api-skeleton/lib/common"
"git.devices.local/mawas/golang-api-skeleton/lib/config"
@ -69,6 +70,13 @@ func startServer(version string, appName string, cfgFile string, flagCfg map[str
if err := c.Set("user:test", "01F5FSJXDHWT4HK93B9NB8V5G4"); err != nil { // username -> userID mapping
return fmt.Errorf("cache:%v", err)
}
// FIXME find out where todo database migration
if err := db.AutoMigrate(&models.Token{}); err != nil {
return fmt.Errorf("database:%v", err)
}
if err := db.AutoMigrate(&models.User{}); err != nil {
return fmt.Errorf("database:%v", err)
}
// Prefill cache
// TODO also add deleted users to cache for old references
// users
@ -76,7 +84,7 @@ func startServer(version string, appName string, cfgFile string, flagCfg map[str
userService := services.NewUserService(userRepo)
userData, err := userService.ReadAll()
if err != nil {
return fmt.Errorf("cache:%v", err)
return fmt.Errorf("cache:%v", err.Error())
}
for i := range userData {
if err := c.Set("user:"+userData[i].ID.String(), userData[i].Username); err != nil { // userID -> username mapping
@ -98,18 +106,16 @@ func startServer(version string, appName string, cfgFile string, flagCfg map[str
return fmt.Errorf("cache:%v", err)
}
}
index, err := indexes.Init(db)
if err != nil {
return fmt.Errorf("index:%v", err)
}
app.Use(inject("index", index))
app.Use(inject("cache", c))
app.Use(inject("db", db))
app.Use(middlewares.RequestID())
app.Use(middlewares.Authentication())
app.Use(middlewares.VersionHeader(version))
// FIXME find out where todo database migration
if err := db.AutoMigrate(&models.Token{}); err != nil {
return fmt.Errorf("database:%v", err)
}
if err := db.AutoMigrate(&models.User{}); err != nil {
return fmt.Errorf("database:%v", err)
}
api.ApplyRoutes(app)
if err := app.Run(fmt.Sprintf("%s:%s", viper.GetString("application.listenaddress"), viper.GetString("application.port"))); err != nil {
return fmt.Errorf("api:%v", err)
@ -117,12 +123,6 @@ func startServer(version string, appName string, cfgFile string, flagCfg map[str
return nil
}
// func ping(c *gin.Context) {
// c.JSON(200, gin.H{
// "message": "pong",
// })
// }
// Inject injects database to gin context
func inject(key string, value interface{}) gin.HandlerFunc {
return func(c *gin.Context) {

3
go.mod

@ -4,9 +4,12 @@ go 1.15
require (
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
github.com/blevesearch/bleve v1.0.14 // indirect
github.com/blevesearch/bleve/v2 v2.0.3 // indirect
github.com/dgraph-io/badger/v2 v2.2007.2
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/gin-gonic/gin v1.7.1 // indirect
github.com/go-errors/errors v1.4.0 // indirect
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
github.com/magiconair/properties v1.8.4 // indirect

92
go.sum

@ -17,6 +17,8 @@ github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/RoaringBitmap/roaring v0.4.23 h1:gpyfd12QohbqhFO4NVDUdoPOCXsyahYRQhINmlHxKeo=
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
@ -30,6 +32,48 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/blevesearch/bleve v1.0.14 h1:Q8r+fHTt35jtGXJUM0ULwM3Tzg+MRfyai4ZkWDy2xO4=
github.com/blevesearch/bleve v1.0.14/go.mod h1:e/LJTr+E7EaoVdkQZTfoz7dt4KoDNvDbLb8MSKuNTLQ=
github.com/blevesearch/bleve/v2 v2.0.3 h1:mDrwrsRIA4PDYkfUNjoh5zGECvquuJIA3MJU5ivaO8E=
github.com/blevesearch/bleve/v2 v2.0.3/go.mod h1:ip+4iafiEq2gCY5rJXe87bT6LkF/OJMCjQEYIfTBfW8=
github.com/blevesearch/bleve_index_api v1.0.0 h1:Ds3XeuTxjXCkG6pgIwWDRyooJKNIuOKemnN0N0IkhTU=
github.com/blevesearch/bleve_index_api v1.0.0/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4=
github.com/blevesearch/blevex v1.0.0/go.mod h1:2rNVqoG2BZI8t1/P1awgTKnGlx5MP9ZbtEciQaNhswc=
github.com/blevesearch/cld2 v0.0.0-20200327141045-8b5f551d37f5/go.mod h1:PN0QNTLs9+j1bKy3d/GB/59wsNBFC4sWLWG3k69lWbc=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/mmap-go v1.0.2 h1:JtMHb+FgQCTTYIhtMvimw15dJwu1Y5lrZDMOFXVWPk0=
github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA=
github.com/blevesearch/scorch_segment_api/v2 v2.0.1 h1:fd+hPtZ8GsbqPK1HslGp7Vhoik4arZteA/IsCEgOisw=
github.com/blevesearch/scorch_segment_api/v2 v2.0.1/go.mod h1:lq7yK2jQy1yQjtjTfU931aVqz7pYxEudHaDwOt1tXfU=
github.com/blevesearch/segment v0.9.0 h1:5lG7yBCx98or7gK2cHMKPukPZ/31Kag7nONpoBt22Ac=
github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ=
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
github.com/blevesearch/upsidedown_store_api v1.0.1 h1:1SYRwyoFLwG3sj0ed89RLtM15amfX2pXlYbFOnF8zNU=
github.com/blevesearch/upsidedown_store_api v1.0.1/go.mod h1:MQDVGpHZrpe3Uy26zJBf/a8h0FZY6xJbthIMm8myH2Q=
github.com/blevesearch/vellum v1.0.3 h1:U86G41A7CtXNzzpIJHM8lSTUqz1Mp8U870TkcdCzZc8=
github.com/blevesearch/vellum v1.0.3/go.mod h1:2u5ax02KeDuNWu4/C+hVQMD6uLN4txH1JbtpaDNLJRo=
github.com/blevesearch/zap/v11 v11.0.14 h1:IrDAvtlzDylh6H2QCmS0OGcN9Hpf6mISJlfKjcwJs7k=
github.com/blevesearch/zap/v11 v11.0.14/go.mod h1:MUEZh6VHGXv1PKx3WnCbdP404LGG2IZVa/L66pyFwnY=
github.com/blevesearch/zap/v12 v12.0.14 h1:2o9iRtl1xaRjsJ1xcqTyLX414qPAwykHNV7wNVmbp3w=
github.com/blevesearch/zap/v12 v12.0.14/go.mod h1:rOnuZOiMKPQj18AEKEHJxuI14236tTQ1ZJz4PAnWlUg=
github.com/blevesearch/zap/v13 v13.0.6 h1:r+VNSVImi9cBhTNNR+Kfl5uiGy8kIbb0JMz/h8r6+O4=
github.com/blevesearch/zap/v13 v13.0.6/go.mod h1:L89gsjdRKGyGrRN6nCpIScCvvkyxvmeDCwZRcjjPCrw=
github.com/blevesearch/zap/v14 v14.0.5 h1:NdcT+81Nvmp2zL+NhwSvGSLh7xNgGL8QRVZ67njR0NU=
github.com/blevesearch/zap/v14 v14.0.5/go.mod h1:bWe8S7tRrSBTIaZ6cLRbgNH4TUDaC9LZSpRGs85AsGY=
github.com/blevesearch/zap/v15 v15.0.3 h1:Ylj8Oe+mo0P25tr9iLPp33lN6d4qcztGjaIsP51UxaY=
github.com/blevesearch/zap/v15 v15.0.3/go.mod h1:iuwQrImsh1WjWJ0Ue2kBqY83a0rFtJTqfa9fp1rbVVU=
github.com/blevesearch/zapx/v11 v11.2.0 h1:GBkCJYsyj3eIU4+aiLPxoMz1PYvDbQZl/oXHIBZIP60=
github.com/blevesearch/zapx/v11 v11.2.0/go.mod h1:gN/a0alGw1FZt/YGTo1G6Z6XpDkeOfujX5exY9sCQQM=
github.com/blevesearch/zapx/v12 v12.2.0 h1:dyRcSoZVO1jktL4UpGkCEF1AYa3xhKPirh4/N+Va+Ww=
github.com/blevesearch/zapx/v12 v12.2.0/go.mod h1:fdjwvCwWWwJW/EYTYGtAp3gBA0geCYGLcVTtJEZnY6A=
github.com/blevesearch/zapx/v13 v13.2.0 h1:mUqbaqQABp8nBE4t4q2qMyHCCq4sykoV8r7aJk4ih3s=
github.com/blevesearch/zapx/v13 v13.2.0/go.mod h1:o5rAy/lRS5JpAbITdrOHBS/TugWYbkcYZTz6VfEinAQ=
github.com/blevesearch/zapx/v14 v14.2.0 h1:UsfRqvM9RJxKNKrkR1U7aYc1cv9MWx719fsAjbF6joI=
github.com/blevesearch/zapx/v14 v14.2.0/go.mod h1:GNgZusc1p4ot040cBQMRGEZobvwjCquiEKYh1xLFK9g=
github.com/blevesearch/zapx/v15 v15.2.0 h1:ZpibwcrrOaeslkOw3sJ7npP7KDgRHI/DkACjKTqFwyM=
github.com/blevesearch/zapx/v15 v15.2.0/go.mod h1:MmQceLpWfME4n1WrBFIwplhWmaQbQqLQARpaKUEOs/A=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@ -44,9 +88,16 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k=
github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
github.com/couchbase/vellum v1.0.2 h1:BrbP0NKiyDdndMPec8Jjhy0U47CZ0Lgx3xUC2r9rZqw=
github.com/couchbase/vellum v1.0.2/go.mod h1:FcwrEivFpNi24R3jLOs3n+fs5RnuQnQqCLBJ1uAg1W4=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -62,6 +113,9 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
@ -71,6 +125,11 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.1 h1:qC89GU3p8TvKWMAVhEpmpB2CIb1hnqt2UdKZaP93mS8=
github.com/gin-gonic/gin v1.7.1/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2 h1:Ujru1hufTHVb++eG6OuNDKMxZnGIvF6o/u8q/8h2+I4=
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/go-errors/errors v1.4.0 h1:2OA7MFw38+e9na72T1xgkomPb6GzZzzxvJ5U630FoRM=
github.com/go-errors/errors v1.4.0/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
@ -106,6 +165,7 @@ github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@ -121,6 +181,7 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@ -146,6 +207,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ikawaha/kagome.ipadic v1.1.2/go.mod h1:DPSBbU0czaJhAb/5uKQZHMc9MTVRpDugJfX+HddPHHg=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
@ -204,6 +267,7 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
@ -214,6 +278,7 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
@ -266,16 +331,24 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc=
github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -292,6 +365,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
@ -338,6 +413,8 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/steveyen/gtreap v0.1.0 h1:CjhzTa274PyJLJuMZwIzCO1PfC00oRa8d1Kc78bFXJM=
github.com/steveyen/gtreap v0.1.0/go.mod h1:kl/5J7XbrOmlIbYIXdRHDDE5QxHqpk0cmkT7Z4dM9/Y=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
@ -348,16 +425,25 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tebeka/snowball v0.4.2/go.mod h1:4IfL14h1lvwZcp1sfXuuc7/7yCsvVffTWxWxCLfFpYg=
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8=
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@ -401,6 +487,7 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -426,10 +513,12 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -446,6 +535,7 @@ golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -510,6 +600,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
@ -517,6 +608,7 @@ gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

62
indexes/indexes.go

@ -0,0 +1,62 @@
package indexes
import (
"fmt"
"git.devices.local/mawas/golang-api-skeleton/lib/common"
"git.devices.local/mawas/golang-api-skeleton/repositories"
"git.devices.local/mawas/golang-api-skeleton/services"
"github.com/blevesearch/bleve"
"gorm.io/gorm"
)
func Init(db *gorm.DB) (bleve.Index, error) {
tokenMapping := bleve.NewDocumentMapping()
tokenMapping.AddFieldMappingsAt("Token", bleve.NewTextFieldMapping())
userMapping := bleve.NewDocumentMapping()
userMapping.AddSubDocumentMapping("Tokens", tokenMapping)
userMapping.AddFieldMappingsAt("Username", bleve.NewTextFieldMapping())
userMapping.AddFieldMappingsAt("Firstname", bleve.NewTextFieldMapping())
userMapping.AddFieldMappingsAt("Lastname", bleve.NewTextFieldMapping())
userMapping.AddFieldMappingsAt("Email", bleve.NewTextFieldMapping())
userMapping.AddFieldMappingsAt("Active", bleve.NewTextFieldMapping())
// indexName := "users.bleve"
mapping := bleve.NewIndexMapping()
mapping.AddDocumentMapping("Thread", userMapping)
// ???? mapping.TypeField = "Username"
// index, err := bleve.New(indexName, mapping)
index, err := bleve.NewMemOnly(mapping)
if err != nil {
return index, err
}
userRepo := repositories.NewUserRepository(db, common.CLIUserID, common.CLIUsername, nil)
userService := services.NewUserService(userRepo)
users, _ := userService.ReadAll()
for i := range users {
id := users[i].ID
// doc, err := json.Marshal(users[i])
// if err != nil {
// return index, err
// }
// fmt.Println(id, string(doc))
err = index.Index(id.String(), users[i])
if err != nil {
return index, err
}
// err = index.SetInternal([]byte(id), doc)
// if err != nil {
// return index, err
// }
}
query := bleve.NewMatchQuery("Mustermann")
search := bleve.NewSearchRequest(query)
searchResults, err := index.Search(search)
if err != nil {
fmt.Println("error", err.Error())
}
fmt.Println(searchResults)
return index, nil
}

168
lib/apperrors/apperrors.go

@ -0,0 +1,168 @@
package apperrors
import (
"net/http"
"strings"
)
type APPError interface {
GetType() ErrorKey
GetHTTPStatus() int
GetDetail() string
SetDetail(string)
Error() string
}
// Error response structure
type Error struct {
Code int `json:"code"`
Type ErrorKey `json:"-"`
Message string `json:"message"`
Detail string `json:"detail"`
HTTPStatus int `json:"-"`
ObjectID uint64 `json:"-"`
ObjectName string `json:"-"`
}
func (err Error) GetType() ErrorKey {
return err.Type
}
func (err Error) GetDetail() string {
return err.Detail
}
func (err Error) SetDetail(detail string) {
err.Detail = detail
}
func (err Error) GetHTTPStatus() int {
return customErrors[err.Type].HTTPStatus
}
func (err Error) Error() string {
return err.Detail
}
func NewError(errType ErrorKey, detail string) (err APPError) {
switch errType {
case DBError, InvalidDBQuery:
detail = humanReadableDatabaseError(detail)
case ValidationFailed:
detail = humanReadableMarshalError(detail)
}
return Error{
Type: errType,
Code: customErrors[errType].Code,
Message: customErrors[errType].Message,
Detail: detail,
HTTPStatus: customErrors[errType].HTTPStatus,
}
}
// customErrors object contains self defined error codes and messages
var customErrors = map[ErrorKey]Error{
ItemNotFound: {
Code: 1000,
Message: MessageItemNotFound,
HTTPStatus: http.StatusNotFound,
},
InvalidID: {
Code: 1001,
Message: MessageInvalidID,
HTTPStatus: http.StatusBadRequest,
},
ValidationFailed: {
Code: 1002,
Message: MessageValidationFailed,
HTTPStatus: http.StatusBadRequest,
},
PathNotFound: {
Code: 4000,
Message: MessagePathNotFound,
HTTPStatus: http.StatusNotFound,
},
MalformedQueryParameter: {
Code: 4001,
Message: MessageMalformedQueryParameter,
HTTPStatus: http.StatusBadRequest,
},
MalformedFilterString: {
Code: 4002,
Message: MessageMalformedFilterString,
HTTPStatus: http.StatusBadRequest,
},
InvalidPagination: {
Code: 4003,
Message: MessageInvalidPagination,
HTTPStatus: http.StatusBadRequest,
},
UnknownError: {
Code: 5000,
Message: MessageUnknownError,
HTTPStatus: http.StatusInternalServerError,
},
DBError: {
Code: 5002,
Message: MessageDBError,
HTTPStatus: http.StatusInternalServerError,
},
InvalidDBQuery: {
Code: 5003,
Message: MessageInvalidDBQuery,
HTTPStatus: http.StatusInternalServerError,
},
EmptyDBResponse: {
Code: 5004,
Message: MessageEmptyDBResponse,
HTTPStatus: http.StatusNotFound,
},
Unauthorized: {
Code: 6000,
Message: MessageUnauthorized,
HTTPStatus: http.StatusUnauthorized,
},
Forbidden: {
Code: 6001,
Message: MessageForbidden,
HTTPStatus: http.StatusForbidden,
},
ReportBug: {
Code: 9999,
Message: MessageReportBug,
HTTPStatus: http.StatusInternalServerError,
},
}
// humanReadableMarshalError translates some cryptic error messages
func humanReadableMarshalError(detail string) string {
switch {
case strings.Contains(detail, "decode slice: expect [ or n, but found {"):
return "payload must be a list"
case strings.Contains(detail, "json: cannot unmarshal object into Go value of type ["):
return "payload must be a list"
case strings.Contains(detail, "strconv.ParseUint: parsing "):
return "id must be numeric"
case strings.Contains(detail, "json: cannot unmarshal array into Go value of type"),
strings.HasPrefix(detail, "readObjectStart: expect { or n, but found ["):
return "payload must be an object"
default:
return detail
}
}
// humanReadableDatabaseError translates some cryptic error messages
func humanReadableDatabaseError(detail string) string {
switch {
case strings.Contains(detail, "Cannot insert duplicate key row in object 'dbo.users' with unique index 'username'."):
return "username already in use, no duplicate usernames allowed"
case strings.Contains(detail, "Cannot insert duplicate key row in object 'dbo.roles' with unique index 'rolename'."):
return "rolename already in use, no duplicate rolenames allowed"
case (strings.Contains(detail, "Duplicate entry") && strings.HasSuffix(detail, "for key 'idx_property_name_tenant_sid'")):
return "propertyname already in use, no duplicate propertynames allowed: " + detail
case strings.Contains(detail, "Cannot insert duplicate key in object 'dbo.tenants'."):
return "Tenant name or id in use, no duplicates allowed"
default:
return detail
}
}

41
lib/apperrors/constants.go

@ -0,0 +1,41 @@
package apperrors
type ErrorKey uint16
const (
NoError ErrorKey = iota
PathNotFound
CacheError
DBError
InvalidDBQuery
EmptyDBResponse
ValidationFailed
UnknownError
InvalidID
MalformedFilterString
MalformedQueryParameter
ItemNotFound
InvalidPagination
Unauthorized
Forbidden
ReportBug
)
// Custom error response messages
const (
MessagePathNotFound = "Path Not Found"
MessageCacheError = "Cache Error"
MessageDBError = "Database Error"
MessageInvalidDBQuery = "Invalid Database Query"
MessageEmptyDBResponse = "Empty DB Response"
MessageValidationFailed = "JSON Body Validation Failed"
MessageUnknownError = "Unknown Error"
MessageInvalidID = "Invalid ID"
MessageMalformedFilterString = "Malformed Filter String"
MessageMalformedQueryParameter = "Malformed Query Parameter"
MessageInvalidPagination = "Invalid Pagination Parameter"
MessageItemNotFound = "Item Not Found"
MessageUnauthorized = "Unauthorized"
MessageForbidden = "Forbidden"
MessageReportBug = "Bug found, please report"
)

5
lib/authentication/authentication.go

@ -1,9 +1,9 @@
package authentication
import (
"errors"
"fmt"
"git.devices.local/mawas/golang-api-skeleton/lib/apperrors"
"git.devices.local/mawas/golang-api-skeleton/lib/response"
"github.com/gin-gonic/gin"
)
@ -13,7 +13,8 @@ func Authorized(c *gin.Context) {
var resp response.Envelope
userID, exists := c.Get("userID")
if !exists {
c.AbortWithStatusJSON(403, resp.AppendError(errors.New("unauthorized")))
e := apperrors.NewError(apperrors.Unauthorized, "")
c.AbortWithStatusJSON(e.GetHTTPStatus(), resp.AppendError(e))
return
}
fmt.Println("permission check", userID)

45
lib/response/response.go

@ -1,11 +1,21 @@
package response
import (
"fmt"
"net/http"
"net/http/httputil"
"git.devices.local/mawas/golang-api-skeleton/lib/apperrors"
"github.com/gin-gonic/gin"
errs "github.com/go-errors/errors"
)
// Envelope for response objects
type Envelope struct {
Success bool `json:"success"`
RequestID string `json:"request_id,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Errors []string `json:"errors,omitempty"`
Success bool `json:"success"`
RequestID string `json:"request_id,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Errors []apperrors.Error `json:"errors,omitempty"`
// PaginationInfo *pagination.Pagination `json:"pagination_info,omitempty"`
Result interface{} `json:"result,omitempty"`
}
@ -14,13 +24,38 @@ type Envelope struct {
func (envelope *Envelope) AppendError(err error) *Envelope {
envelope.Success = false
envelope.Result = nil
envelope.Errors = append(envelope.Errors, err.Error())
envelope.Errors = append(envelope.Errors, err.(apperrors.Error))
return envelope
}
// HTTPError to envelope
func (envelope *Envelope) HTTPError(err error) (int, *Envelope) {
envelope.Success = false
envelope.Result = nil
if e, ok := err.(apperrors.Error); ok {
envelope.Errors = append(envelope.Errors, e)
return e.HTTPStatus, envelope
}
e := apperrors.NewError(apperrors.UnknownError, err.Error())
envelope.Errors = append(envelope.Errors, e.(apperrors.Error))
return http.StatusInternalServerError, envelope
}
// SetSuccess to envelope
func (envelope *Envelope) SetSuccess(result interface{}) *Envelope {
envelope.Success = true
envelope.Result = result
return envelope
}
// Recovery returns with JSON error after panic
func (envelope *Envelope) Recovery(c *gin.Context) {
if err := recover(); err != nil {
httprequest, _ := httputil.DumpRequest(c.Request, false)
goErr := errs.Wrap(err, 3)
reset := string([]byte{27, 91, 48, 109})
fmt.Printf("panic recovered:\n\n%s%s\n\n%s%s", httprequest, goErr.Error(), goErr.Stack(), reset)
envelope.AppendError(apperrors.NewError(apperrors.UnknownError, fmt.Sprintf("%s\n%s", goErr.Error(), goErr.Stack())))
c.AbortWithStatusJSON(500, envelope)
}
}

13
lib/response/response_test.go

@ -1,14 +1,15 @@
package response
import (
"errors"
"testing"
"git.devices.local/mawas/golang-api-skeleton/lib/apperrors"
)
func TestAppendError(t *testing.T) {
const errMsg = "expected error"
envelope := Envelope{}
e := envelope.AppendError(errors.New(errMsg))
var envelope Envelope
err := apperrors.NewError(apperrors.DBError, "expected error")
e := envelope.AppendError(err)
if e.Success {
t.Error("error response Success must be false")
}
@ -17,8 +18,8 @@ func TestAppendError(t *testing.T) {
}
if len(e.Errors) != 1 {
t.Error("error response Errors must be set")
} else if e.Errors[0] != errMsg {
t.Errorf("error response error message must be \"%s\" but is \"%s\"", errMsg, e.Errors[0])
} else if e.Errors[0] != err {
t.Errorf("error response error message must be \"%s\" but is \"%s\"", err, e.Errors[0])
}
}

3
main.go

@ -12,7 +12,7 @@ const defaultHost = "unknown"
// https://towardsdatascience.com/building-restful-apis-in-golang-e3fe6e3f8f95
// https://www.voile.com/voile-straps.html
// https://yusufs.medium.com/creating-distributed-kv-database-by-implementing-raft-consensus-using-golang-d0884eef2e28 RAFT to create badgerdb sync?
// https://yusufs.medium.com/creating-distributed-kv-database-by-implementing-raft-consensus-using-golang-d0884eef2e28 RAFT to create badgerdb sync? dragonboat?
// FIXME how to set unique index for username to make soft deletes possible but disallow duplicates... also how to cache a username which got delete and recerated... user ID would get overwritten, maybe pre/suffix deleted username?
// TODO add command for db migrate? no auto migrate?
@ -25,6 +25,7 @@ const defaultHost = "unknown"
// TODO add logger to gin
// TODO make explicit getDBConnection function
// TODO restructure flags e.g. listenaddress and port to server
// FIXME https://medium.com/telnet/why-should-you-start-reading-documentation-from-limitations-section-6690c08e5d1a#.em5kfkezd - too many SQL variables
var (
Version string // allows to set version on build

37
repositories/token_test.go

@ -42,14 +42,14 @@ func tokenmock(t *testing.T) (models.Token, *TokenDAO) {
t.Error(err)
}
userDAO := NewUserRepository(db, userID.String(), username, appCache)
_, err = userDAO.Create(&models.User{
_, err = userDAO.Create([]*models.User{{
ModelHiddenGUIDPK: common.ModelHiddenGUIDPK{ID: userID},
Username: username,
Firstname: "Unit",
Lastname: "Test",
Email: "unittest@unittest.test",
Active: true,
})
}})
if err != nil {
t.Error(err)
}
@ -61,7 +61,7 @@ func tokenmock(t *testing.T) (models.Token, *TokenDAO) {
return token, NewTokenRepository(db, token.UserID.String(), username, appCache)
}
func validateResponse(t *testing.T, response *models.Token, token models.Token) {
func validateTokenResponse(t *testing.T, response *models.Token, token models.Token) {
if response.Username != token.Username {
t.Errorf("Token Username must be \"%s\" but is \"%s\"\n", token.Username, response.Username)
}
@ -120,22 +120,25 @@ func validateResponse(t *testing.T, response *models.Token, token models.Token)
func TestTokenRepository(t *testing.T) {
token, tokenDAO := tokenmock(t)
response, err := tokenDAO.Create(&token)
if err != nil {
if response, err := tokenDAO.Create(&token); err != nil {
t.Errorf("tokenDAO Create failed with error: %v\n", err)
} else {
validateTokenResponse(t, response, token)
}
validateResponse(t, response, token)
response, err = tokenDAO.ReadByKey(token.Token.String())
if err != nil {
if response, err := tokenDAO.ReadByKey(token.Token.String()); err != nil {
t.Errorf("tokenDAO ReadByKey failed with error: %v\n", err)
} else {
validateTokenResponse(t, response, token)
}
{
response, err := tokenDAO.ReadAll()
switch {
case err != nil:
t.Errorf("tokenDAO ReadAll failed with error: %v\n", err)
case len(response) != 1:
t.Error("tokenDAO ReadAll failed with worng numer of responses")
default:
validateTokenResponse(t, response[0], token)
}
}
validateResponse(t, response, token)
responses, err := tokenDAO.ReadAll()
if err != nil {
t.Errorf("tokenDAO ReadAll failed with error: %v\n", err)
}
if len(responses) != 1 {
t.Error("tokenDAO ReadAll failed with worng numer of responses")
}
validateResponse(t, responses[0], token)
}

46
repositories/user.go

@ -2,8 +2,10 @@
package repositories
import (
"git.devices.local/mawas/golang-api-skeleton/lib/apperrors"
"git.devices.local/mawas/golang-api-skeleton/lib/cache"
model "git.devices.local/mawas/golang-api-skeleton/models"
"git.devices.local/mawas/golang-api-skeleton/lib/common"
"git.devices.local/mawas/golang-api-skeleton/models"
"gorm.io/gorm"
)
@ -23,19 +25,39 @@ func NewUserRepository(db *gorm.DB, userID string, username string, cache cache.
}
}
func (dao *UserDAO) ReadByUsername(username string) (*model.User, error) {
result := model.User{}
err := dao.db.Preload("TokensRef").Where(&model.User{Username: username}).First(&result).Error
return &result, err
func (dao *UserDAO) ReadByUsername(username string) (*models.User, error) {
result := models.User{}
if err := dao.db.Preload("TokensRef").Where(&models.User{Username: username}).First(&result).Error; err != nil {
return nil, apperrors.NewError(apperrors.DBError, err.Error())
}
return &result, nil
}
func (dao *UserDAO) ReadAll() ([]*models.User, error) {
var result []*models.User
if err := dao.db.Preload("TokensRef").Find(&result).Error; err != nil {
return nil, apperrors.NewError(apperrors.DBError, err.Error())
}
return result, nil
}
func (dao *UserDAO) ReadAll() ([]*model.User, error) {
var result []*model.User
err := dao.db.Preload("TokensRef").Find(&result).Error
return result, err
func (dao *UserDAO) ReadSelection(ids []common.GUID) ([]*models.User, error) {
var result []*models.User
if err := dao.db.Preload("TokensRef").Where("id IN (?)", ids).Find(&result).Error; err != nil {
return nil, apperrors.NewError(apperrors.DBError, err.Error())
}
return result, nil
}
func (dao *UserDAO) Create(user *model.User) (*model.User, error) {
err := dao.db.Create(user).Error
return user, err
func (dao *UserDAO) Create(users []*models.User) ([]*models.User, error) {
db := dao.db.Begin()
for i := range users {
err := db.Create(users[i]).Error
if err != nil {
db.Rollback()
return nil, apperrors.NewError(apperrors.DBError, err.Error())
}
}
db.Commit()
return users, nil
}

41
repositories/user_test.go

@ -116,22 +116,31 @@ func validateUserResponse(t *testing.T, response *models.User, user models.User)
func TestUserRepository(t *testing.T) {
user, userDAO := userRepositoryMock(t)
response, err := userDAO.Create(&user)
if err != nil {
t.Errorf("userDAO Create failed with error: %v\n", err)
}
validateUserResponse(t, response, user)
response, err = userDAO.ReadByUsername(username)
if err != nil {
{
response, err := userDAO.Create([]*models.User{&user})
switch {
case err != nil:
t.Errorf("userDAO Create failed with error: %v\n", err)
case len(response) != 1:
t.Error("userDAO Create response must be a list with 1 result in this test")
default:
validateUserResponse(t, response[0], user)
}
}
if response, err := userDAO.ReadByUsername(username); err != nil {
t.Errorf("userDAO ReadByUsername failed with error: %v\n", err)
} else {
validateUserResponse(t, response, user)
}
{
response, err := userDAO.ReadAll()
switch {
case err != nil:
t.Errorf("userDAO ReadAll failed with error: %v\n", err)
case len(response) != 1:
t.Error("userDAO ReadAll failed with worng number of responses")
default:
validateUserResponse(t, response[0], user)
}
}
validateUserResponse(t, response, user)
responses, err := userDAO.ReadAll()
if err != nil {
t.Errorf("userDAO ReadAll failed with error: %v\n", err)
}
if len(responses) != 1 {
t.Error("userDAO ReadAll failed with worng numer of responses")
}
validateUserResponse(t, responses[0], user)
}

35
services/token_test.go

@ -44,14 +44,14 @@ func tokenServiceMock(t *testing.T) (models.Token, *TokenService) {
}
userRepository := repositories.NewUserRepository(db, userID.String(), username, appCache)
userService := NewUserService(userRepository)
_, err = userService.Create(&models.User{
_, err = userService.Create([]*models.User{{
ModelHiddenGUIDPK: common.ModelHiddenGUIDPK{ID: userID},
Username: username,
Firstname: "Unit",
Lastname: "Test",
Email: "unittest@unittest.test",
Active: true,
})
}})
if err != nil {
t.Error(err)
}
@ -120,22 +120,25 @@ func validateResponse(t *testing.T, response *models.Token, token models.Token)
func TestTokenRepository(t *testing.T) {
token, tokenService := tokenServiceMock(t)
response, err := tokenService.Create(&token)
if err != nil {
if response, err := tokenService.Create(&token); err != nil {
t.Errorf("tokenService Create failed with error: %v\n", err)
} else {
validateResponse(t, response, token)
}
validateResponse(t, response, token)
response, err = tokenService.ReadByKey(token.Token.String())
if err != nil {
if response, err := tokenService.ReadByKey(token.Token.String()); err != nil {
t.Errorf("tokenService ReadByKey failed with error: %v\n", err)
} else {
validateResponse(t, response, token)
}
{
response, err := tokenService.ReadAll()
switch {
case err != nil:
t.Errorf("tokenService ReadAll failed with error: %v\n", err)
case len(response) != 1:
t.Error("tokenService ReadAll failed with worng numer of responses")
default:
validateResponse(t, response[0], token)
}
}
validateResponse(t, response, token)
responses, err := tokenService.ReadAll()
if err != nil {
t.Errorf("tokenService ReadAll failed with error: %v\n", err)
}
if len(responses) != 1 {
t.Error("tokenService ReadAll failed with worng numer of responses")
}
validateResponse(t, responses[0], token)
}

27
services/user.go

@ -1,13 +1,15 @@
package services
import (
model "git.devices.local/mawas/golang-api-skeleton/models"
"git.devices.local/mawas/golang-api-skeleton/lib/common"
"git.devices.local/mawas/golang-api-skeleton/models"
)
type userRepository interface {
ReadByUsername(username string) (*model.User, error)
ReadAll() ([]*model.User, error)
Create(user *model.User) (*model.User, error)
ReadByUsername(username string) (*models.User, error)
ReadAll() ([]*models.User, error)
ReadSelection(ids []common.GUID) ([]*models.User, error)
Create(users []*models.User) ([]*models.User, error)
}
type UserService struct {
@ -20,23 +22,28 @@ func NewUserService(repo userRepository) *UserService {
}
// ReadByUsername just retrieves user using User DAO, here can be additional logic for processing data retrieved by DAOs
func (s *UserService) ReadByID(id interface{}) (*model.User, error) {
func (s *UserService) ReadByID(id interface{}) (*models.User, error) {
return s.repo.ReadByUsername(id.(string))
}
// ReadAll just retrieves user using User DAO, here can be additional logic for processing data retrieved by DAOs
func (s *UserService) ReadAll() ([]*model.User, error) {
func (s *UserService) ReadAll() ([]*models.User, error) {
return s.repo.ReadAll()
}
// ReadAll just retrieves user using User DAO, here can be additional logic for processing data retrieved by DAOs
func (s *UserService) ReadSelection(ids []common.GUID) ([]*models.User, error) {
return s.repo.ReadSelection(ids)
}
// Create just retrieves user using User DAO, here can be additional logic for processing data retrieved by DAOs
func (s *UserService) Create(user *model.User) (*model.User, error) {
return s.repo.Create(user)
func (s *UserService) Create(users []*models.User) ([]*models.User, error) {
return s.repo.Create(users)
}
// NewUser returns a new created User object
func NewUser(username, firstname, lastname, email string) model.User {
return model.User{
func NewUser(username, firstname, lastname, email string) models.User {
return models.User{
Username: username,
Firstname: firstname,
Lastname: lastname,

37
services/user_test.go

@ -112,22 +112,29 @@ func validateUserResponse(t *testing.T, response *models.User, user models.User)
func TestUserRepository(t *testing.T) {
user, userService := userServiceMock(t)
response, err := userService.Create(&user)
if err != nil {
if response, err := userService.Create([]*models.User{&user}); err != nil {
t.Errorf("userService Create failed with error: %v\n", err)
}
validateUserResponse(t, response, user)
response, err = userService.ReadByID(username)
if err != nil {
} else {
if len(response) != 1 {
t.Error("userService Create response must be a list with 1 result in this test")
} else {
validateUserResponse(t, response[0], user)
}
}
if response, err := userService.ReadByID(username); err != nil {
t.Errorf("userService ReadByID failed with error: %v\n", err)
} else {
validateUserResponse(t, response, user)
}
{
response, err := userService.ReadAll()
switch {
case err != nil:
t.Errorf("userService ReadAll failed with error: %v\n", err)
case len(response) != 1:
t.Error("userService ReadAll failed with worng numer of responses")
default:
validateUserResponse(t, response[0], user)
}
}
validateUserResponse(t, response, user)
responses, err := userService.ReadAll()
if err != nil {
t.Errorf("userService ReadAll failed with error: %v\n", err)
}
if len(responses) != 1 {
t.Error("userService ReadAll failed with worng numer of responses")
}
validateUserResponse(t, responses[0], user)
}
Loading…
Cancel
Save