├── .dockerignore ├── .gitignore ├── .gometalinter ├── Dockerfile ├── api ├── auth.go ├── auth_test.go ├── context.go ├── cronjob.go ├── db_helper.go ├── handler.go ├── handler_test.go ├── user.go ├── user_test.go └── webhook.go ├── config ├── config.go └── config_test.go ├── core ├── db.go ├── error.go ├── json.go ├── test.go ├── timestamp.go └── type.go ├── db ├── dbconf.yml ├── migrations │ └── 20190611174624_base.sql └── seed.sql ├── go.mod ├── go.sum ├── log ├── hook.go ├── log.go ├── logger.go └── logstash.go ├── main.go ├── makefile ├── middleware ├── configuration.go ├── db.go ├── elastic.go ├── header.go ├── initialize.go ├── session.go └── type.go ├── model ├── configuration.go ├── context.go ├── creator.go ├── creator_test.go ├── db_test.go ├── deleter.go ├── deleter_test.go ├── getter.go ├── getter_test.go ├── jwt.go ├── jwt_test.go ├── model.go ├── restrictor.go ├── restrictor_test.go ├── sorter.go ├── sorter_test.go ├── test_helper.go ├── updater.go ├── updater_test.go ├── user.go ├── user_test.go ├── validator.go └── validator_test.go ├── server ├── router.go └── server.go └── util ├── helper.go └── helper_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | README.md 2 | .vscode 3 | .git 4 | .go-pkg-cache 5 | .gopath 6 | vendor -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | golang-boilerplate 2 | 3 | *~ 4 | *.out 5 | *.zip 6 | *.orig 7 | *.dat 8 | .DS_Store 9 | dump.rdb 10 | *.zip 11 | *.csv 12 | #*# 13 | 14 | # Elastic Beanstalk Files 15 | .elasticbeanstalk/* 16 | !.elasticbeanstalk/*.cfg.yml 17 | !.elasticbeanstalk/*.global.yml 18 | environment 19 | main.env 20 | vendor 21 | vendor/* 22 | _vendor 23 | .vendor 24 | 25 | .idea/* 26 | .vscode 27 | npm-debug.log -------------------------------------------------------------------------------- /.gometalinter: -------------------------------------------------------------------------------- 1 | { 2 | "Exclude": ["exported \\w+ (\\S*['.]*)([a-zA-Z'.*]*) should have comment or be unexported"] 3 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.12.5-alpine as builder 2 | 3 | ARG COMMIT_REF 4 | ARG BUILD_DATE 5 | 6 | ENV APP_COMMIT_REF=${COMMIT_REF} \ 7 | APP_BUILD_DATE=${BUILD_DATE} 8 | 9 | RUN apk update && apk upgrade && \ 10 | apk add --no-cache bash git openssh g++ glide ca-certificates 11 | 12 | RUN adduser -D -g '' appuser 13 | 14 | RUN mkdir -p /go/src/github.com/brunoksato/golang-boilerplate 15 | ADD . /go/src/github.com/brunoksato/golang-boilerplate 16 | WORKDIR /go/src/github.com/brunoksato/golang-boilerplate 17 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o golang-boilerplate . 18 | 19 | FROM scratch 20 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 21 | COPY --from=builder /etc/passwd /etc/passwd 22 | COPY --from=builder /go/src/github.com/brunoksato/golang-boilerplate/golang-boilerplate /app/ 23 | WORKDIR /app 24 | EXPOSE 8080 25 | CMD ["./golang-boilerplate"] 26 | -------------------------------------------------------------------------------- /api/auth.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | 7 | "github.com/brunoksato/golang-boilerplate/core" 8 | log "github.com/brunoksato/golang-boilerplate/log" 9 | "github.com/brunoksato/golang-boilerplate/model" 10 | "github.com/dgrijalva/jwt-go" 11 | "github.com/labstack/echo/v4" 12 | "golang.org/x/crypto/bcrypt" 13 | ) 14 | 15 | func SignUp(c echo.Context) error { 16 | ctx := ServerContext(c) 17 | db := ctx.Database 18 | tx := db.Begin() 19 | 20 | if err := tx.Error; err != nil { 21 | tx.Rollback() 22 | return log.AddDefaultError(c, core.NewServerError(err.Error())) 23 | } 24 | 25 | user := model.User{} 26 | if err := c.Bind(&user); err != nil { 27 | tx.Rollback() 28 | return log.AddDefaultError(c, core.NewServerError(err.Error())) 29 | } 30 | 31 | cerr := user.Create(ArgonContextTransaction(c, tx), ctx.User) 32 | if cerr != nil { 33 | tx.Rollback() 34 | return log.AddDefaultError(c, cerr) 35 | } 36 | 37 | err := tx.Where("email = ?", user.Email).Find(&user).Error 38 | if err != nil { 39 | tx.Rollback() 40 | return log.AddDefaultError(c, core.NewServerError(err.Error())) 41 | } 42 | 43 | tx.Commit() 44 | 45 | ctx.Payload["results"] = user 46 | return c.JSON(http.StatusCreated, ctx.Payload) 47 | } 48 | 49 | func SignIn(c echo.Context) error { 50 | ctx := ServerContext(c) 51 | db := ctx.Database 52 | 53 | u := model.User{} 54 | if err := c.Bind(&u); err != nil { 55 | return log.AddDefaultError(c, core.NewServerError(err.Error())) 56 | } 57 | 58 | if u.Username != "" { 59 | if err := db.Where("username = ?", u.Username).Find(&ctx.User).Error; err != nil { 60 | return log.AddDefaultError(c, core.NewServerError(err.Error())) 61 | } 62 | 63 | if ctx.APIType == core.ADMIN_API && !ctx.User.IsAdmin() { 64 | return c.JSON(http.StatusUnauthorized, map[string]interface{}{"status": "Not Authorized"}) 65 | } 66 | 67 | if ok, _ := ctx.User.VerifyPassword(u.Password); ok { 68 | expireAt := model.JWTTokenExpirationDate() 69 | 70 | jwt, dberr := model.IssueJWToken(ctx.User.ID, []string{"user"}, expireAt) 71 | if dberr != nil { 72 | return log.AddDefaultError(c, 73 | core.NewServerError( 74 | dberr.Error(), 75 | map[string]interface{}{ 76 | "user_id": ctx.User.ID, 77 | }, 78 | ), 79 | ) 80 | } 81 | 82 | ctx.Payload["results"] = ctx.User 83 | ctx.Payload["token"] = jwt 84 | return c.JSON(http.StatusOK, ctx.Payload) 85 | } 86 | } 87 | 88 | return c.JSON(http.StatusUnauthorized, map[string]interface{}{"status": "Not Authorized"}) 89 | } 90 | 91 | func RecoverPassword(c echo.Context) error { 92 | ctx := ServerContext(c) 93 | db := ctx.Database 94 | 95 | email := c.Param("email") 96 | 97 | user := model.User{} 98 | count := 0 99 | 100 | err := db.Where("LOWER(email) = LOWER(?)", email).Find(&user).Count(&count).Error 101 | if err != nil { 102 | return c.JSON(http.StatusBadRequest, map[string]interface{}{"status": "Email Not Found"}) 103 | } 104 | 105 | if count > 0 { 106 | expireAt := model.JWTTokenExpirationDate() 107 | jwt, dberr := model.IssueJWTTokenForEmail(user.ID, user.Email, expireAt) 108 | if dberr != nil { 109 | return log.AddDefaultError(c, 110 | core.NewServerError( 111 | dberr.Error(), 112 | map[string]interface{}{ 113 | "user_id": ctx.User.ID, 114 | }, 115 | ), 116 | ) 117 | } 118 | 119 | user.ResetPasswordEmail(jwt) 120 | } 121 | 122 | return c.JSON(http.StatusOK, map[string]interface{}{"status": "OK"}) 123 | } 124 | 125 | func ChangePasswordExternal(c echo.Context) error { 126 | ctx := ServerContext(c) 127 | db := ctx.Database 128 | 129 | type customPasswordExternal struct { 130 | Token string `json:"token"` 131 | Password string `json:"password"` 132 | } 133 | 134 | u := new(customPasswordExternal) 135 | if err := c.Bind(&u); err != nil { 136 | return log.AddDefaultError(c, core.NewServerError(err.Error())) 137 | } 138 | 139 | secretKey := os.Getenv("JWT_KEY_EMAIL") 140 | token, err := model.VerifyJWTToken(u.Token, secretKey) 141 | if err != nil { 142 | return c.JSON(http.StatusBadRequest, map[string]interface{}{"message": "Token invalid"}) 143 | } 144 | 145 | if err != nil { 146 | log.LoggerForParams(c, 147 | map[string]interface{}{ 148 | "user_id": ctx.User.ID, 149 | "status": "Token invalid", 150 | "code": http.StatusBadRequest, 151 | }, 152 | ).Info("ChangePasswordExternal: Token inválido") 153 | return c.JSON(http.StatusBadRequest, map[string]interface{}{"message": "Token invalid"}) 154 | } 155 | 156 | claims := token.Claims.(jwt.MapClaims) 157 | if token.Valid && claims["iss"] == model.JWT_ISS { 158 | email := claims["email"].(string) 159 | 160 | user := model.User{} 161 | err := db.Where("email = ?", email).First(&user).Error 162 | if err != nil { 163 | return log.AddDefaultError(c, core.NewServerError(err.Error())) 164 | } 165 | 166 | hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) 167 | uerr := db.Model(&user).Set("gorm:save_associations", false).UpdateColumn("hashed_password", hashedPassword).Error 168 | if uerr != nil { 169 | return log.AddDefaultError(c, core.NewServerError(uerr.Error())) 170 | } 171 | } else { 172 | return c.JSON(http.StatusBadRequest, map[string]interface{}{"message": "Token invalid or expired"}) 173 | } 174 | 175 | return c.JSON(http.StatusOK, map[string]interface{}{"status": "OK"}) 176 | } 177 | -------------------------------------------------------------------------------- /api/auth_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/brunoksato/golang-boilerplate/core" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSignIn(t *testing.T) { 11 | setup() 12 | defer teardown() 13 | router := router() 14 | 15 | u := map[string]interface{}{ 16 | "username": "system", 17 | "password": "123456", 18 | } 19 | 20 | rw, req := core.NewTestPost("POST", "/public/signin", u) 21 | 22 | router.ServeHTTP(rw, req) 23 | core.AssertResponseCode(t, rw, 200) 24 | 25 | actualInt := core.JsonToMap(rw.Body.String()) 26 | actual := actualInt["results"].(map[string]interface{}) 27 | assert.Equal(t, actual["username"], "system") 28 | assert.Equal(t, actual["email"], "system@model.com") 29 | } 30 | 31 | func TestSignInWrong(t *testing.T) { 32 | setup() 33 | defer teardown() 34 | router := router() 35 | 36 | u := map[string]interface{}{ 37 | "email": "testuser@model.com", 38 | "password": "password123", 39 | } 40 | 41 | rw, req := core.NewTestPost("POST", "/public/signin", u) 42 | 43 | router.ServeHTTP(rw, req) 44 | core.AssertResponseCode(t, rw, 401) 45 | } 46 | 47 | func TestSignUp(t *testing.T) { 48 | setup() 49 | defer teardown() 50 | router := router() 51 | 52 | u := map[string]interface{}{ 53 | "name": "bruno sato", 54 | "username": "brunoksato", 55 | "email": "brunosato@model.com", 56 | "password": "password", 57 | "phone": "12982575000", 58 | } 59 | 60 | rw, req := core.NewTestPost("POST", "/public/signup", u) 61 | 62 | router.ServeHTTP(rw, req) 63 | core.AssertResponseCode(t, rw, 201) 64 | 65 | actualInt := core.JsonToMap(rw.Body.String()) 66 | actual := actualInt["results"].(map[string]interface{}) 67 | assert.Equal(t, actual["email"], "brunosato@model.com") 68 | assert.Equal(t, actual["name"], "bruno sato") 69 | assert.Equal(t, actual["username"], "brunoksato") 70 | assert.Equal(t, actual["phone"], "12982575000") 71 | } 72 | -------------------------------------------------------------------------------- /api/context.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | 7 | "github.com/Sirupsen/logrus" 8 | "github.com/brunoksato/golang-boilerplate/model" 9 | "github.com/brunoksato/golang-boilerplate/core" 10 | log "github.com/brunoksato/golang-boilerplate/log" 11 | "github.com/jinzhu/gorm" 12 | "github.com/labstack/echo/v4" 13 | "github.com/olivere/elastic" 14 | ) 15 | 16 | type Context struct { 17 | Database *gorm.DB 18 | Elastic *elastic.Client 19 | Logger *logrus.Entry 20 | Payload map[string]interface{} 21 | Request map[string]interface{} 22 | Type reflect.Type 23 | ParentType reflect.Type 24 | User model.User 25 | AppName string 26 | Method string 27 | Path string 28 | Endpoint string 29 | RequestID string 30 | Configuration model.Configuration 31 | APIType core.APIType 32 | ModelCtx *model.ModelCtx 33 | } 34 | 35 | func ServerContext(c echo.Context) *Context { 36 | var t reflect.Type 37 | var parentType reflect.Type 38 | var user model.User 39 | var APIType core.APIType 40 | var configuration model.Configuration 41 | 42 | if c.Get("User") != nil { 43 | user = c.Get("User").(model.User) 44 | } 45 | 46 | if c.Get("Type") != nil { 47 | t = c.Get("Type").(reflect.Type) 48 | } 49 | 50 | if c.Get("ParentType") != nil { 51 | parentType = c.Get("ParentType").(reflect.Type) 52 | } 53 | 54 | if c.Get("APIType") != nil { 55 | APIType = c.Get("APIType").(core.APIType) 56 | } 57 | 58 | if c.Get("Configuration") != nil { 59 | configuration = c.Get("Configuration").(model.Configuration) 60 | } 61 | 62 | return &Context{ 63 | RequestID: c.Get("RequestID").(string), 64 | Database: c.Get("Database").(*gorm.DB), 65 | Elastic: c.Get("Elastic").(*elastic.Client), 66 | User: user, 67 | Configuration: configuration, 68 | Logger: log.Logger(c), 69 | Type: t, 70 | ParentType: parentType, 71 | Payload: make(map[string]interface{}), 72 | Request: make(map[string]interface{}), 73 | APIType: APIType, 74 | } 75 | } 76 | 77 | func ArgonContext(c echo.Context) *model.ModelCtx { 78 | ctx := ServerContext(c) 79 | if ctx.ModelCtx == nil { 80 | c.Logger().Debug("Creating a new ArgonContext.") 81 | ctx.ModelCtx = &model.ModelCtx{ 82 | RequestID: ctx.RequestID, 83 | APIType: ctx.APIType, 84 | Database: ctx.Database, 85 | Configuration: c.Get("Configuration").(model.Configuration), 86 | User: c.Get("User").(model.User), 87 | Logger: log.Logger(c), 88 | } 89 | } 90 | 91 | return ctx.ModelCtx 92 | } 93 | 94 | func ArgonContextTransaction(c echo.Context, db *gorm.DB) *model.ModelCtx { 95 | ctx := ServerContext(c) 96 | if ctx.ModelCtx == nil { 97 | c.Logger().Debug("Creating a new ArgonContext.") 98 | ctx.ModelCtx = &model.ModelCtx{ 99 | RequestID: ctx.RequestID, 100 | APIType: ctx.APIType, 101 | Database: db, 102 | Configuration: c.Get("Configuration").(model.Configuration), 103 | User: c.Get("User").(model.User), 104 | Logger: log.Logger(c), 105 | } 106 | } 107 | 108 | ctx.Database = db 109 | 110 | return ctx.ModelCtx 111 | } 112 | 113 | func ActiveUserID(c echo.Context, ctx *Context) uint { 114 | userID := ctx.User.ID 115 | uid, _ := strconv.Atoi(c.Param("userId")) 116 | if uid > 0 { 117 | userID = uint(uid) 118 | } 119 | return userID 120 | } 121 | -------------------------------------------------------------------------------- /api/cronjob.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/brunoksato/golang-boilerplate/core" 7 | log "github.com/brunoksato/golang-boilerplate/log" 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func CronJobSample(c echo.Context) error { 12 | ctx := ServerContext(c) 13 | db := ctx.Database 14 | 15 | tx := db.Begin() 16 | if err := tx.Error; err != nil { 17 | tx.Rollback() 18 | return log.AddDefaultError(c, core.NewServerError(err.Error())) 19 | } 20 | 21 | tx.Commit() 22 | 23 | return c.JSON(http.StatusOK, map[string]interface{}{"status": "ok"}) 24 | } 25 | -------------------------------------------------------------------------------- /api/db_helper.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/brunoksato/golang-boilerplate/model" 9 | "github.com/brunoksato/golang-boilerplate/core" 10 | "github.com/jinzhu/gorm" 11 | ) 12 | 13 | func OrderByFor(db *gorm.DB, t reflect.Type) *gorm.DB { 14 | if model.IsSorter(t) { 15 | item := reflect.New(t).Interface() 16 | sorter := item.(model.Sorter) 17 | return sorter.OrderBy(db) 18 | } 19 | 20 | tableName := db.NewScope(reflect.New(t).Interface()).TableName() 21 | tableName = fmt.Sprintf("\"%s\".", tableName) 22 | order := tableName + "\"id\" ASC" 23 | 24 | _, orderField := model.OrderField(t) 25 | if orderField != "" { 26 | order = fmt.Sprintf("%s\"%s\" ASC, %s", tableName, orderField, order) 27 | } 28 | 29 | return db.Order(order) 30 | } 31 | 32 | func JoinsFor(ctx *Context, db *gorm.DB, parentIsSpecific bool) *gorm.DB { 33 | return db.Scopes(core.DefaultPreloads(db, ctx.Type, ctx.APIType, parentIsSpecific)) 34 | } 35 | 36 | func ScopesFor(ctx *Context, path string, userID uint, db *gorm.DB) *gorm.DB { 37 | parts := strings.Split(path, "/") 38 | for i := len(parts) - 1; i >= 0; i-- { 39 | part := parts[i] 40 | switch part { 41 | //change the name of model 42 | case "models": 43 | db = db.Where("user_id = ?", userID) 44 | } 45 | 46 | } 47 | return db 48 | } 49 | 50 | func FilterByParentFor(db *gorm.DB, pt, t reflect.Type, parentID uint) *gorm.DB { 51 | userType := reflect.TypeOf(model.User{}) 52 | 53 | if pt == userType { 54 | db = FilterByUserFor(db, pt, t, parentID) 55 | } else if pt != nil { 56 | parentField := gorm.ToDBName(pt.Name()) 57 | db = db.Where(parentField+"_id = ?", parentID) 58 | } else if model.TypeHasParentField(t) { 59 | _, parentField := model.ParentIdField(t) 60 | db = db.Where(parentField+" = ?", parentID) 61 | } 62 | 63 | return db 64 | } 65 | 66 | func FilterByUserFor(db *gorm.DB, pt, t reflect.Type, userID uint) *gorm.DB { 67 | var zeroType reflect.Type 68 | 69 | ptName := "" 70 | if pt != zeroType { 71 | ptName = pt.Name() 72 | } 73 | 74 | switch ptName { 75 | case "", "User": 76 | switch t.Name() { 77 | default: 78 | _, userIDField := model.UserIDField(t) 79 | if userIDField == "user_id" { 80 | db = db.Where("user_id = ?", userID) 81 | } 82 | } 83 | } 84 | 85 | return db 86 | } 87 | -------------------------------------------------------------------------------- /api/handler.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "reflect" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/brunoksato/golang-boilerplate/model" 12 | "github.com/brunoksato/golang-boilerplate/core" 13 | log "github.com/brunoksato/golang-boilerplate/log" 14 | "github.com/brunoksato/golang-boilerplate/util" 15 | "github.com/jinzhu/gorm" 16 | "github.com/labstack/echo/v4" 17 | ) 18 | 19 | func List(c echo.Context) error { 20 | ctx := ServerContext(c) 21 | 22 | db := ctx.Database 23 | 24 | db, err := DefaultListQuery(c, ctx, db) 25 | if err != nil { 26 | return log.AddDefaultError(c, err) 27 | } 28 | 29 | db = DefaultJoins(c, ctx, db) 30 | db = DefaultScopes(c, ctx, db) 31 | db = DefaultPaging(c, ctx, db) 32 | db = DefaultOrder(c, ctx, db) 33 | 34 | err = AddListToPayload(ctx, db) 35 | if err != nil { 36 | return log.AddDefaultError(c, err) 37 | } 38 | 39 | return c.JSON(http.StatusOK, ctx.Payload) 40 | } 41 | 42 | func Create(c echo.Context) error { 43 | ctx := ServerContext(c) 44 | db := ctx.Database 45 | 46 | item := reflect.New(ctx.Type).Interface() 47 | if err := c.Bind(item); err != nil { 48 | return log.AddDefaultError(c, core.NewServerError(err.Error())) 49 | } 50 | 51 | userID := ActiveUserID(c, ctx) 52 | if userID > 0 { 53 | model.SetUserID(item, uint(userID)) 54 | } 55 | 56 | if model.IsCreator(reflect.PtrTo(ctx.Type)) { 57 | creator := item.(model.Creator) 58 | err := creator.Create(ArgonContext(c), ctx.User) 59 | if err != nil { 60 | return log.AddDefaultError(c, err) 61 | } 62 | item = creator 63 | } else { 64 | err := DefaultValidationForCreate(c, ctx, item) 65 | if err != nil { 66 | return log.AddDefaultError(c, err) 67 | } 68 | 69 | dberr := db.Set("gorm:save_associations", false).Create(item).Error 70 | if dberr != nil { 71 | return log.AddDefaultError(c, core.NewServerError(dberr.Error())) 72 | } 73 | } 74 | 75 | db.First(item) 76 | 77 | ctx.Payload["results"] = item 78 | return c.JSON(http.StatusCreated, ctx.Payload) 79 | } 80 | 81 | func Get(c echo.Context) error { 82 | ctx := ServerContext(c) 83 | db := ctx.Database 84 | 85 | id, err := strconv.Atoi(c.Param("id")) 86 | if err != nil { 87 | return log.AddDefaultError(c, core.NewNotFoundError(err.Error())) 88 | } 89 | 90 | item := reflect.New(ctx.Type).Interface() 91 | 92 | if model.IsGetter(ctx.Type) && err != nil { 93 | var merr core.DefaultError 94 | getter := item.(model.Getter) 95 | item, merr = getter.GetByID(ArgonContext(c), ctx.User, uint(id)) 96 | if merr != nil { 97 | return log.AddDefaultError(c, merr) 98 | } 99 | } else { 100 | db = DefaultJoins(c, ctx, db) 101 | db = DefaultScopes(c, ctx, db) 102 | err = db.First(item, id).Error 103 | 104 | if err != nil { 105 | return log.AddDefaultError(c, core.NewNotFoundError(err.Error())) 106 | } 107 | 108 | merr := DefaultValidationForGet(c, item) 109 | if merr != nil { 110 | return log.AddDefaultError(c, merr) 111 | } 112 | } 113 | 114 | ctx.Payload["results"] = item 115 | return c.JSON(http.StatusOK, ctx.Payload) 116 | } 117 | 118 | func Update(c echo.Context) error { 119 | ctx := ServerContext(c) 120 | db := ctx.Database 121 | 122 | id, err := strconv.Atoi(c.Param("id")) 123 | if err != nil { 124 | return log.AddDefaultError(c, core.NewNotFoundError(err.Error())) 125 | } 126 | 127 | item := reflect.New(ctx.Type).Interface() 128 | err = db.First(item, id).Error 129 | if err != nil { 130 | return log.AddDefaultError(c, core.NewNotFoundError(err.Error())) 131 | } 132 | 133 | if err := c.Bind(item); err != nil { 134 | return log.AddDefaultError(c, core.NewServerError(err.Error())) 135 | } 136 | 137 | requestMap := core.ModelToJsonMap(item) 138 | 139 | fields := make([]string, 0) 140 | for fieldName, newVal := range requestMap { 141 | typeField, _ := core.GetFieldByJsonTag(item, fieldName) 142 | if typeField != nil && shouldUpdateField(*typeField) { 143 | core.SetByJsonTag(item, fieldName, newVal) 144 | fields = append(fields, typeField.Name) 145 | } 146 | } 147 | 148 | if model.IsUpdater(reflect.PtrTo(ctx.Type)) { 149 | updater := item.(model.Updater) 150 | merr := updater.Update(ArgonContext(c), ctx.User) 151 | if merr != nil { 152 | return log.AddDefaultError(c, merr) 153 | } 154 | item = updater 155 | } else { 156 | merr := DefaultValidationForUpdate(c, ctx, item, fields) 157 | if merr != nil { 158 | return log.AddDefaultError(c, merr) 159 | } 160 | 161 | dberr := db.Set("gorm:save_associations", false).Save(item).Error 162 | if dberr != nil { 163 | return log.AddDefaultError(c, core.NewServerError("Error saving data: "+dberr.Error())) 164 | } 165 | } 166 | 167 | db.First(item) 168 | 169 | ctx.Payload["results"] = item 170 | return c.JSON(http.StatusAccepted, ctx.Payload) 171 | } 172 | 173 | func Delete(c echo.Context) error { 174 | ctx := ServerContext(c) 175 | db := ctx.Database 176 | 177 | id, err := strconv.Atoi(c.Param("id")) 178 | if err != nil { 179 | return log.AddDefaultError(c, core.NewNotFoundError(err.Error())) 180 | } 181 | 182 | item := reflect.New(ctx.Type).Interface() 183 | err = db.First(item, id).Error 184 | if err != nil { 185 | return log.AddDefaultError(c, core.NewNotFoundError(err.Error())) 186 | } 187 | 188 | var merr core.DefaultError 189 | if model.IsDeleter(reflect.PtrTo(ctx.Type)) { 190 | deleter := item.(model.Deleter) 191 | merr = deleter.Delete(ArgonContext(c), ctx.User) 192 | } else { 193 | merr := DefaultValidationForDelete(c, ctx, item) 194 | if merr != nil { 195 | return log.AddDefaultError(c, merr) 196 | } 197 | 198 | merr = model.DefaultDelete(ArgonContext(c), item) 199 | if merr != nil { 200 | return log.AddDefaultError(c, merr) 201 | } 202 | } 203 | 204 | if merr != nil { 205 | return log.AddDefaultError(c, merr) 206 | } 207 | 208 | ctx.Payload["results"] = item 209 | return c.JSON(http.StatusOK, ctx.Payload) 210 | } 211 | 212 | func DefaultListQuery(c echo.Context, ctx *Context, db *gorm.DB) (*gorm.DB, core.DefaultError) { 213 | var parentID, userID int 214 | strParentID := c.Param("parentId") 215 | if strParentID != "" { 216 | var err error 217 | parentID, err = strconv.Atoi(strParentID) 218 | if err != nil { 219 | return db, core.NewNotFoundError(fmt.Sprintf("Invalid id: %s", strParentID)) 220 | } 221 | if parentID <= 0 { 222 | return db, core.NewNotFoundError(fmt.Sprintf("Invalid id: %s", strParentID)) 223 | } 224 | } 225 | strUserID := c.Param("userId") 226 | if strUserID != "" { 227 | var err error 228 | userID, err = strconv.Atoi(strUserID) 229 | if err != nil { 230 | return db, core.NewNotFoundError(fmt.Sprintf("Invalid id: %s", strUserID)) 231 | } 232 | if userID <= 0 { 233 | return db, core.NewNotFoundError(fmt.Sprintf("Invalid id: %s", strUserID)) 234 | } 235 | } 236 | 237 | if parentID > 0 { 238 | db = FilterByParentFor(db, ctx.ParentType, ctx.Type, uint(parentID)) 239 | } 240 | 241 | if userID > 0 { 242 | db = FilterByUserFor(db, ctx.ParentType, ctx.Type, uint(userID)) 243 | } 244 | 245 | switch ctx.APIType { 246 | case core.USER_API: 247 | userID := ActiveUserID(c, ctx) 248 | if ctx.ParentType == reflect.TypeOf(model.User{}) { 249 | db = FilterByUserFor(db, ctx.ParentType, ctx.Type, userID) 250 | } 251 | case core.ADMIN_API: 252 | // no filter 253 | } 254 | return db, nil 255 | } 256 | 257 | func DefaultJoins(c echo.Context, ctx *Context, db *gorm.DB) *gorm.DB { 258 | parentID, _ := strconv.Atoi(c.Param("parentId")) 259 | 260 | if parentID > 0 { 261 | db = JoinsFor(ctx, db, true) 262 | } else { 263 | db = JoinsFor(ctx, db, false) 264 | } 265 | return db 266 | } 267 | 268 | func DefaultScopes(c echo.Context, ctx *Context, db *gorm.DB) *gorm.DB { 269 | userID := ActiveUserID(c, ctx) 270 | path := c.Path() 271 | db = ScopesFor(ctx, path, userID, db) 272 | return db 273 | } 274 | 275 | func DefaultPaging(c echo.Context, ctx *Context, db *gorm.DB, opts ...bool) *gorm.DB { 276 | queryTC := true 277 | if len(opts) > 0 { 278 | queryTC = opts[0] 279 | } 280 | 281 | st := c.QueryParam("start") 282 | limit, _ := strconv.Atoi(c.QueryParam("limit")) 283 | 284 | if limit > 0 && queryTC { 285 | queryTotalCount(ctx, db) 286 | } 287 | 288 | if st != "" { 289 | startIdx, _ := strconv.Atoi(st) 290 | if startIdx > 0 { 291 | db = db.Offset(startIdx) 292 | } 293 | } 294 | 295 | if limit > 0 { 296 | db = limitQueryByConfig(ctx, db, "", limit) 297 | } 298 | 299 | return db 300 | } 301 | 302 | func queryTotalCount(ctx *Context, db *gorm.DB) { 303 | item := reflect.New(ctx.Type).Interface() 304 | var n int 305 | 306 | db.Model(item). 307 | Select("COUNT(*)"). 308 | Row(). 309 | Scan(&n) 310 | 311 | ctx.Payload["ct"] = n 312 | } 313 | 314 | func limitQueryByConfig(c *Context, db *gorm.DB, key string, requestLimit int) *gorm.DB { 315 | dbLimit := requestLimit 316 | limitStr := os.Getenv(key) 317 | limit, err := strconv.Atoi(limitStr) 318 | if err == nil { 319 | if dbLimit <= 0 || (limit > 0 && limit < dbLimit) { 320 | dbLimit = limit 321 | } 322 | } 323 | if dbLimit > 0 { 324 | db = db.Limit(dbLimit) 325 | } 326 | return db 327 | } 328 | 329 | func DefaultOrder(c echo.Context, ctx *Context, db *gorm.DB) *gorm.DB { 330 | sort := c.FormValue("sort") 331 | if sort != "" { 332 | fields, ascending := util.ConvertQueryTermToOrderTerm(sort) 333 | tableName := db.NewScope(reflect.New(ctx.Type).Interface()).TableName() 334 | order := "" 335 | for i, field := range fields { 336 | term := "" 337 | switch field { 338 | case "email": 339 | term = fmt.Sprintf("\"%s\".email", tableName) 340 | case "id": 341 | term = fmt.Sprintf("\"%s\".id", tableName) 342 | case "created_at": 343 | term = fmt.Sprintf("\"%s\".id", tableName) 344 | case "updated_at": 345 | term = fmt.Sprintf("\"%s\".id", tableName) 346 | default: 347 | item := reflect.New(ctx.Type).Interface() 348 | structField, err := core.GetFieldByJsonTag(item, field) 349 | if err == nil { 350 | term = fmt.Sprintf("\"%s\".%s", tableName, gorm.ToDBName(structField.Name)) 351 | } else { 352 | continue 353 | } 354 | } 355 | 356 | if ascending[i] { 357 | term = fmt.Sprintf("%s ASC", term) 358 | } else { 359 | term = fmt.Sprintf("%s DESC", term) 360 | } 361 | 362 | order = fmt.Sprintf("%s %s,", order, term) 363 | } 364 | 365 | order = fmt.Sprintf("%s %s.id ASC", order, tableName) 366 | db = db.Order(order) 367 | } else { 368 | db = OrderByFor(db, ctx.Type) 369 | } 370 | return db 371 | } 372 | 373 | func AddListToPayload(ctx *Context, db *gorm.DB) core.DefaultError { 374 | result, err := getListFromQuery(ctx, db) 375 | if err != nil { 376 | if err.IsWarning() { 377 | ctx.Payload["results"] = result 378 | } 379 | return err 380 | } 381 | 382 | ctx.Payload["results"] = result 383 | return nil 384 | } 385 | 386 | func DefaultValidationForGet(c echo.Context, item interface{}) core.DefaultError { 387 | ctx := ServerContext(c) 388 | 389 | switch ctx.Type.Name() { 390 | case "Example": 391 | // Don't restrict the basic types 392 | default: 393 | if model.IsRestrictor(ctx.Type) { 394 | restrictor := item.(model.Restrictor) 395 | canView, err := restrictor.UserCanView(ArgonContext(c), ctx.User) 396 | if err != nil { 397 | return err 398 | } 399 | if !canView { 400 | return core.NewPermissionError("You do not have permission", 401 | core.ERROR_SUBCODE_USER_LACKS_PERMISSION) 402 | } 403 | } 404 | } 405 | return nil 406 | } 407 | 408 | func DefaultValidationForCreate(c echo.Context, ctx *Context, item interface{}) core.DefaultError { 409 | if model.IsRestrictor(ctx.Type) { 410 | restrictor := item.(model.Restrictor) 411 | canCreate, err := restrictor.UserCanCreate(ArgonContext(c), ctx.User) 412 | if err != nil { 413 | return err 414 | } 415 | if !canCreate { 416 | return core.NewPermissionError("You do not have permission", 417 | core.ERROR_SUBCODE_USER_LACKS_PERMISSION) 418 | } 419 | } 420 | 421 | if model.IsValidator(ctx.Type) { 422 | validator := item.(model.Validator) 423 | return validator.ValidateForCreate() 424 | } 425 | 426 | return nil 427 | } 428 | 429 | func DefaultValidationForUpdate(c echo.Context, ctx *Context, item interface{}, fields []string) core.DefaultError { 430 | if model.IsRestrictor(ctx.Type) { 431 | restrictor := item.(model.Restrictor) 432 | canUpdate, err := restrictor.UserCanUpdate(ArgonContext(c), ctx.User, fields) 433 | if err != nil { 434 | return err 435 | } 436 | if !canUpdate { 437 | return core.NewPermissionError("You do not have permission to make these changes", 438 | core.ERROR_SUBCODE_USER_LACKS_PERMISSION) 439 | } 440 | } 441 | 442 | if model.IsValidator(ctx.Type) { 443 | err := model.ValidateStructFields(item, fields) 444 | if err != nil { 445 | return core.NewBusinessError(err.Error()) 446 | } 447 | return nil 448 | } 449 | 450 | return nil 451 | } 452 | 453 | func DefaultValidationForDelete(c echo.Context, ctx *Context, item interface{}) core.DefaultError { 454 | if model.IsRestrictor(ctx.Type) { 455 | restrictor := item.(model.Restrictor) 456 | canDelete, err := restrictor.UserCanDelete(ArgonContext(c), ctx.User) 457 | if err != nil { 458 | return err 459 | } 460 | if !canDelete { 461 | return core.NewPermissionError("You do not have permission", 462 | core.ERROR_SUBCODE_USER_LACKS_PERMISSION) 463 | } 464 | } 465 | 466 | if model.IsValidator(ctx.Type) { 467 | validator := item.(model.Validator) 468 | return validator.ValidateForDelete(ArgonContext(c)) 469 | } 470 | 471 | return nil 472 | } 473 | 474 | func getListFromQuery(ctx *Context, db *gorm.DB) (interface{}, core.DefaultError) { 475 | var err error 476 | 477 | items := util.NewSliceForType(ctx.Type) 478 | err = db.Find(items).Error 479 | if err != nil { 480 | return nil, core.NewServerError(err.Error()) 481 | } 482 | 483 | items = util.ItemsOrEmptySlice(ctx.Type, items) 484 | 485 | return items, nil 486 | } 487 | 488 | func shouldUpdateField(field reflect.StructField) bool { 489 | tag := field.Tag 490 | if tag.Get("settable") == "false" { 491 | return false 492 | } 493 | 494 | timeType := reflect.TypeOf(time.Time{}) 495 | timestampType := reflect.TypeOf(core.Timestamp{}) 496 | nullableTimestampType := reflect.TypeOf(core.NullableTimestamp{}) 497 | 498 | should := true 499 | typ := field.Type 500 | kind := typ.Kind() 501 | 502 | if kind == reflect.Ptr { 503 | typ = field.Type.Elem() 504 | kind = typ.Kind() 505 | } 506 | 507 | should = should && kind != reflect.Array 508 | should = should && kind != reflect.Slice 509 | should = should && kind != reflect.Struct 510 | should = should || typ == timeType 511 | should = should || typ == timestampType 512 | should = should || typ == nullableTimestampType 513 | return should 514 | } 515 | -------------------------------------------------------------------------------- /api/handler_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/brunoksato/golang-boilerplate/api" 8 | middle "github.com/brunoksato/golang-boilerplate/middleware" 9 | "github.com/brunoksato/golang-boilerplate/model" 10 | "github.com/brunoksato/golang-boilerplate/server" 11 | "github.com/jinzhu/gorm" 12 | "github.com/labstack/echo/v4" 13 | "github.com/labstack/echo/v4/middleware" 14 | "golang.org/x/crypto/bcrypt" 15 | ) 16 | 17 | var CTX *api.Context 18 | var INITDB *gorm.DB 19 | var TESTDB *gorm.DB 20 | var ROLE string = "user" 21 | 22 | func init() { 23 | os.Setenv("TEST_ON", "true") 24 | INITDB = model.InitTestDB() 25 | } 26 | 27 | func setup() { 28 | TESTDB = INITDB.Begin() 29 | model.DeleteAllCommitedEntities(TESTDB) 30 | model.SeedDatabase(TESTDB) 31 | } 32 | 33 | func setAdminRole() { 34 | ROLE = "admin" 35 | } 36 | 37 | func startTransaction() { 38 | TESTDB = INITDB.Begin() 39 | CTX.Database = TESTDB 40 | } 41 | 42 | func teardown() { 43 | TESTDB = TESTDB.Rollback() 44 | } 45 | 46 | func startDBLog() { 47 | TESTDB.LogMode(true) 48 | } 49 | 50 | func stopDBLog() { 51 | TESTDB.LogMode(false) 52 | } 53 | 54 | func router() *echo.Echo { 55 | return server.SetupRouter(TestMiddlewareConfigurer{}) 56 | } 57 | 58 | func createTestUser() { 59 | password := "123456" 60 | 61 | hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 62 | 63 | user := model.User{ 64 | Model: model.Model{ID: 999}, 65 | Email: "system@model.com", 66 | Name: "system", 67 | Username: "system", 68 | Phone: "12982573000", 69 | HashedPassword: hashedPassword, 70 | } 71 | err := TESTDB.Create(&user).Error 72 | if err != nil { 73 | panic(fmt.Sprintf("Error creating test user: %s", err)) 74 | } 75 | } 76 | 77 | type TestMiddlewareConfigurer struct{} 78 | 79 | func (mc TestMiddlewareConfigurer) ConfigureDefaultApiMiddleware(root *echo.Echo) *echo.Echo { 80 | root.Use(middleware.Recover()) 81 | root.Use(middleware.CORS()) 82 | root.Use(middleware.SecureWithConfig(middleware.SecureConfig{ 83 | XSSProtection: "1; mode=block", 84 | ContentTypeNosniff: "nosniff", 85 | XFrameOptions: "SAMEORIGIN", 86 | ContentSecurityPolicy: "default-src 'self'", 87 | })) 88 | root.Use(middle.DBMiddleware(TESTDB)) 89 | root.Use(middle.ElasticMiddleware(nil)) 90 | root.Use(middle.DetermineType) 91 | root.Use(middle.InitializePayload) 92 | root.Use(middle.LoadConfigurations) 93 | 94 | return root 95 | } 96 | 97 | func (mc TestMiddlewareConfigurer) ConfigurePublicApiMiddleware(root *echo.Echo) *echo.Group { 98 | api := mc.ConfigureDefaultApiMiddleware(root) 99 | public := api.Group("/public") 100 | public.Use(middleware.CORSWithConfig(middleware.CORSConfig{AllowOrigins: []string{"*"}})) 101 | public.Use(middle.Session) 102 | 103 | return public 104 | } 105 | 106 | func (mc TestMiddlewareConfigurer) ConfigurePrivateApiMiddleware(root *echo.Echo) *echo.Group { 107 | api := mc.ConfigureDefaultApiMiddleware(root) 108 | private := api.Group("/api") 109 | private.Use(middleware.Gzip()) 110 | private.Use(middle.SettingHeaders) 111 | private.Use(middleware.CORSWithConfig(middleware.CORSConfig{AllowOrigins: []string{"*"}})) 112 | private.Use(SetUpTestUser(ROLE)) 113 | private.Use(SetTestUserToken) 114 | private.Use(middle.Session) 115 | 116 | return private 117 | } 118 | 119 | func (mc TestMiddlewareConfigurer) ConfigureCronJobApiMiddleware(root *echo.Echo) *echo.Group { 120 | api := mc.ConfigureDefaultApiMiddleware(root) 121 | private := api.Group("/cronjob") 122 | private.Use(middleware.CORS()) 123 | private.Use(middleware.Gzip()) 124 | private.Use(middle.SettingHeaders) 125 | private.Use(middleware.CORSWithConfig(middleware.CORSConfig{AllowOrigins: []string{"*"}})) 126 | private.Use(SetUpTestUser(ROLE)) 127 | private.Use(SetTestUserToken) 128 | private.Use(middle.Session) 129 | 130 | return private 131 | } 132 | 133 | func (mc TestMiddlewareConfigurer) ConfigureAdminApiMiddleware(root *echo.Echo) *echo.Group { 134 | api := mc.ConfigureDefaultApiMiddleware(root) 135 | private := api.Group("/admin") 136 | private.Use(middleware.CORS()) 137 | private.Use(middleware.Gzip()) 138 | private.Use(middle.SettingHeaders) 139 | private.Use(middleware.CORSWithConfig(middleware.CORSConfig{AllowOrigins: []string{"*"}})) 140 | private.Use(SetUpTestUser(ROLE)) 141 | private.Use(SetTestUserToken) 142 | private.Use(middle.Session) 143 | 144 | return private 145 | } 146 | 147 | func SetUpTestUser(role string) echo.MiddlewareFunc { 148 | return func(next echo.HandlerFunc) echo.HandlerFunc { 149 | return func(c echo.Context) error { 150 | password := "password" 151 | hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 152 | 153 | user := model.User{} 154 | dbFind := TESTDB.Unscoped().First(&user, 999) 155 | if dbFind.RecordNotFound() { 156 | user := model.User{ 157 | Model: model.Model{ID: 999}, 158 | Email: "system@model.com", 159 | Name: "system", 160 | Username: "system", 161 | Phone: "12982573000", 162 | HashedPassword: hashedPassword, 163 | } 164 | err := TESTDB.Create(&user).Error 165 | if err != nil { 166 | panic(fmt.Sprintf("Error creating test user: %s", err)) 167 | } 168 | } else { 169 | user.HashedPassword = hashedPassword 170 | TESTDB.Unscoped().Save(&user) 171 | } 172 | 173 | c.Set("User", user) 174 | 175 | return next(c) 176 | } 177 | } 178 | } 179 | 180 | func SetTestUserToken(next echo.HandlerFunc) echo.HandlerFunc { 181 | return func(c echo.Context) error { 182 | user := c.Get("User").(model.User) 183 | expireAt := model.JWTTokenExpirationDate() 184 | jwt, _ := model.IssueJWToken(user.ID, []string{"user"}, expireAt) 185 | c.Request().Header.Add("Authorization", fmt.Sprintf("Bearer %s", jwt)) 186 | return next(c) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /api/user.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/brunoksato/golang-boilerplate/core" 7 | log "github.com/brunoksato/golang-boilerplate/log" 8 | "github.com/brunoksato/golang-boilerplate/model" 9 | "github.com/labstack/echo/v4" 10 | "golang.org/x/crypto/bcrypt" 11 | ) 12 | 13 | func Logout(c echo.Context) error { 14 | return c.JSON(http.StatusOK, map[string]interface{}{"status": "OK"}) 15 | } 16 | 17 | func Me(c echo.Context) error { 18 | ctx := ServerContext(c) 19 | ctx.Payload["results"] = ctx.User 20 | return c.JSON(http.StatusOK, ctx.Payload) 21 | } 22 | 23 | func ChangePassword(c echo.Context) error { 24 | ctx := ServerContext(c) 25 | db := ctx.Database 26 | 27 | type customPassword struct { 28 | Password string `json:"password"` 29 | } 30 | 31 | u := new(customPassword) 32 | if err := c.Bind(&u); err != nil { 33 | return log.AddDefaultError(c, core.NewServerError(err.Error())) 34 | } 35 | 36 | user := model.User{} 37 | err := db.First(&user, ctx.User.ID).Error 38 | if err != nil { 39 | return log.AddDefaultError(c, core.NewServerError(err.Error())) 40 | } 41 | 42 | user.HashedPassword, _ = bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) 43 | 44 | serr := db.Model(&user).Set("gorm:save_associations", false).UpdateColumn("hashed_password", user.HashedPassword).Error 45 | if serr != nil { 46 | return log.AddDefaultError(c, core.NewServerError(serr.Error())) 47 | } 48 | 49 | return c.JSON(http.StatusOK, user) 50 | } 51 | 52 | func UpdateUser(c echo.Context) error { 53 | ctx := ServerContext(c) 54 | db := ctx.Database 55 | 56 | user := model.User{} 57 | err := db.First(&user, ctx.User.ID).Error 58 | if err != nil { 59 | return log.AddDefaultError(c, core.NewNotFoundError(err.Error())) 60 | } 61 | 62 | if err := c.Bind(&user); err != nil { 63 | return log.AddDefaultError(c, core.NewServerError(err.Error())) 64 | } 65 | 66 | dberr := db. 67 | Model(&user). 68 | Set("gorm:save_associations", false). 69 | Updates(map[string]interface{}{ 70 | "name": user.Name, 71 | "email": user.Email, 72 | "phone": user.Phone, 73 | "image": user.Image, 74 | }).Error 75 | if dberr != nil { 76 | return log.AddDefaultError(c, core.NewServerError(dberr.Error())) 77 | } 78 | 79 | return c.JSON(http.StatusOK, map[string]interface{}{"status": "OK"}) 80 | } 81 | -------------------------------------------------------------------------------- /api/user_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/brunoksato/golang-boilerplate/core" 7 | "github.com/brunoksato/golang-boilerplate/model" 8 | "github.com/stretchr/testify/assert" 9 | "golang.org/x/crypto/bcrypt" 10 | ) 11 | 12 | func TestUserDirects(t *testing.T) { 13 | setup() 14 | defer teardown() 15 | router := router() 16 | 17 | hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost) 18 | 19 | u1 := model.User{Name: "User1", Phone: "12982573000", Email: "user1@model.com", Username: "user1", HashedPassword: hashedPassword} 20 | u2 := model.User{Name: "User2", Phone: "12982573000", Email: "user2@model.com", Username: "user2", HashedPassword: hashedPassword} 21 | u3 := model.User{Name: "User3", Phone: "12982573000", Email: "user3@model.com", Username: "user3", HashedPassword: hashedPassword} 22 | u4 := model.User{Name: "User4", Phone: "12982573000", Email: "user4@model.com", Username: "user4", HashedPassword: hashedPassword} 23 | TESTDB.Create(&u1) 24 | TESTDB.Create(&u2) 25 | TESTDB.Create(&u3) 26 | TESTDB.Create(&u4) 27 | u5 := model.User{Name: "User5", Email: "user5@model.com", Username: "user5", HashedPassword: hashedPassword} 28 | u6 := model.User{Name: "User6", Email: "user6@model.com", Username: "user6", HashedPassword: hashedPassword} 29 | TESTDB.Create(&u5) 30 | TESTDB.Create(&u6) 31 | 32 | rw, req := core.NewTestRequest("GET", "/api/users/directs") 33 | router.ServeHTTP(rw, req) 34 | core.AssertResponseCode(t, rw, 200) 35 | 36 | actualInt := core.JsonToMap(rw.Body.String()) 37 | actual := actualInt["results"].([]interface{}) 38 | assert.Equal(t, 4, len(actual)) 39 | assert.Equal(t, actual[0].(map[string]interface{})["username"], "user1") 40 | assert.Equal(t, actual[1].(map[string]interface{})["username"], "user2") 41 | assert.Equal(t, actual[2].(map[string]interface{})["username"], "user3") 42 | assert.Equal(t, actual[3].(map[string]interface{})["username"], "user4") 43 | } 44 | -------------------------------------------------------------------------------- /api/webhook.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/brunoksato/golang-boilerplate/core" 7 | log "github.com/brunoksato/golang-boilerplate/log" 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func WebhookSample(c echo.Context) error { 12 | ctx := ServerContext(c) 13 | db := ctx.Database 14 | tx := db.Begin() 15 | 16 | ctx.Logger.Info("WebhookSample Start") 17 | 18 | if err := tx.Error; err != nil { 19 | tx.Rollback() 20 | return log.AddDefaultError(c, core.NewServerError(err.Error())) 21 | } 22 | 23 | event := map[string]interface{}{} 24 | if err := c.Bind(&event); err != nil { 25 | return log.AddDefaultError(c, core.NewServerError(err.Error())) 26 | } 27 | 28 | tx.Commit() 29 | 30 | ctx.Logger.Info("WebhookSample End") 31 | 32 | return c.JSON(http.StatusOK, map[string]interface{}{"status": "ok"}) 33 | } 34 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | "github.com/Sirupsen/logrus" 10 | "github.com/brunoksato/golang-boilerplate/log" 11 | "github.com/jinzhu/gorm" 12 | _ "github.com/jinzhu/gorm/dialects/postgres" 13 | elastic "github.com/olivere/elastic" 14 | "go.elastic.co/apm/module/apmgorm" 15 | ) 16 | 17 | var CONFIGURATIONS map[string]string = map[string]string{ 18 | "SERVER_ENV": "development", 19 | "SERVER_NAME": "SERVER", 20 | "DATABASE_URL": "dbname=server sslmode=disable", 21 | "AWS_ACCESS_KEY": "", 22 | "AWS_ACCESS_KEY_SECRET": "", 23 | "AWS_REGION": "us-east-1", 24 | "AWS_BUCKET": "development.company.asset", 25 | "ES_HOST": "localhost:9200", 26 | "LOGGER_LEVEL": "info", 27 | "SENDGRID_KEY": "key_sendgrid", 28 | "SENDGRID_USER": "support@company.com", 29 | "JWT_KEY_SIGNIN": "you_secret_key", 30 | "JWT_KEY_EMAIL": "you_secret_key_email", 31 | "JWT_TOKEN_EXPIRATION": "72", 32 | } 33 | 34 | func Init() { 35 | if os.Getenv("SERVER_ENV") == "" { 36 | os.Setenv("SERVER_ENV", CONFIGURATIONS["SERVER_ENV"]) 37 | } 38 | if os.Getenv("SERVER_NAME") == "" { 39 | os.Setenv("SERVER_NAME", CONFIGURATIONS["SERVER_NAME"]) 40 | } 41 | if os.Getenv("DATABASE_URL") == "" { 42 | os.Setenv("DATABASE_URL", CONFIGURATIONS["DATABASE_URL"]) 43 | } 44 | if os.Getenv("AWS_ACCESS_KEY") == "" { 45 | os.Setenv("AWS_ACCESS_KEY", CONFIGURATIONS["AWS_ACCESS_KEY"]) 46 | } 47 | if os.Getenv("AWS_ACCESS_KEY_SECRET") == "" { 48 | os.Setenv("AWS_ACCESS_KEY_SECRET", CONFIGURATIONS["AWS_ACCESS_KEY_SECRET"]) 49 | } 50 | if os.Getenv("AWS_REGION") == "" { 51 | os.Setenv("AWS_REGION", CONFIGURATIONS["AWS_REGION"]) 52 | } 53 | if os.Getenv("AWS_BUCKET") == "" { 54 | os.Setenv("AWS_BUCKET", CONFIGURATIONS["AWS_BUCKET"]) 55 | } 56 | if os.Getenv("ES_HOST") == "" { 57 | os.Setenv("ES_HOST", CONFIGURATIONS["ES_HOST"]) 58 | } 59 | if os.Getenv("LOGGER_LEVEL") == "" { 60 | os.Setenv("LOGGER_LEVEL", CONFIGURATIONS["LOGGER_LEVEL"]) 61 | } 62 | if os.Getenv("SENDGRID_KEY") == "" { 63 | os.Setenv("SENDGRID_KEY", CONFIGURATIONS["SENDGRID_KEY"]) 64 | } 65 | if os.Getenv("SENDGRID_USER") == "" { 66 | os.Setenv("SENDGRID_USER", CONFIGURATIONS["SENDGRID_USER"]) 67 | } 68 | if os.Getenv("JWT_KEY_SIGNIN") == "" { 69 | os.Setenv("JWT_KEY_SIGNIN", CONFIGURATIONS["JWT_KEY_SIGNIN"]) 70 | } 71 | if os.Getenv("JWT_KEY_EMAIL") == "" { 72 | os.Setenv("JWT_KEY_EMAIL", CONFIGURATIONS["JWT_KEY_EMAIL"]) 73 | } 74 | if os.Getenv("JWT_TOKEN_EXPIRATION") == "" { 75 | os.Setenv("JWT_TOKEN_EXPIRATION", CONFIGURATIONS["JWT_TOKEN_EXPIRATION"]) 76 | } 77 | } 78 | 79 | func InitDB() *gorm.DB { 80 | if os.Getenv("DATABASE_URL") == "" { 81 | os.Setenv("DATABASE_URL", CONFIGURATIONS["DATABASE_URL"]) 82 | } 83 | 84 | db, err := apmgorm.Open("postgres", os.Getenv("DATABASE_URL")) 85 | if err != nil { 86 | panic(err.Error()) 87 | } 88 | 89 | fmt.Println(fmt.Sprintf("Initialized read-write database connection pool: %s", os.Getenv("DATABASE_URL"))) 90 | return db 91 | } 92 | 93 | func InitElasticSearchAndLogger() (client *elastic.Client) { 94 | logrus.SetFormatter(&log.LogstashFormatter{}) 95 | // Acceptable values are: 96 | // debug, info, warn, error, fatal, panic 97 | level := os.Getenv("LOGGER_LEVEL") 98 | if level != "" { 99 | l, err := logrus.ParseLevel(level) 100 | if err == nil { 101 | logrus.SetLevel(l) 102 | } else { 103 | fmt.Println("Error with log level configuraion:", err) 104 | } 105 | } 106 | 107 | appName := fmt.Sprintf("%s-%s", os.Getenv("NAME"), os.Getenv("ENV")) 108 | esHostname := os.Getenv("ES_HOST") 109 | thisHostname, _ := os.Hostname() 110 | 111 | if esHostname != "" { 112 | var esURL string 113 | if strings.Contains(esHostname, "127.0.0.1") { 114 | esURL = fmt.Sprintf("http://%s", esHostname) 115 | } else { 116 | esURL = fmt.Sprintf("https://%s", esHostname) 117 | } 118 | fmt.Println(fmt.Sprintf("Configuring elasticsearch logging: %s", esURL)) 119 | client, err := elastic.NewClient(elastic.SetSniff(false), elastic.SetURL(esURL)) 120 | if err != nil { 121 | fmt.Println(fmt.Sprintf("Error configuring elasticsearch logging: %s", err.Error())) 122 | } else { 123 | now := time.Now() 124 | appName = fmt.Sprintf("logstash-%d.%d.%d", now.Year(), now.Month(), now.Day()) 125 | hook, err := log.NewElasticHook(client, thisHostname, logrus.DebugLevel, appName) 126 | if err == nil { 127 | logrus.AddHook(hook) 128 | } else { 129 | fmt.Println(fmt.Sprintf("Error configuring logger for elastic search: %s", err.Error())) 130 | } 131 | } 132 | 133 | return client 134 | } 135 | 136 | return 137 | } 138 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | -------------------------------------------------------------------------------- /core/db.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/jinzhu/gorm" 9 | ) 10 | 11 | func DefaultPreloads(db *gorm.DB, t reflect.Type, api APIType, skipParent bool) func(*gorm.DB) *gorm.DB { 12 | return func(db *gorm.DB) *gorm.DB { 13 | elemT := t 14 | if elemT.Kind() == reflect.Ptr { 15 | elemT = elemT.Elem() 16 | } 17 | for i := 0; i < elemT.NumField(); i++ { 18 | f := elemT.Field(i) 19 | ft := f.Type 20 | if ft.Kind() == reflect.Ptr { 21 | ft = ft.Elem() 22 | } 23 | 24 | preload := false 25 | all := false 26 | skip := false 27 | fieldName := f.Name 28 | tag := elemT.Field(i).Tag 29 | 30 | if skipParent { 31 | if _, ok := tag.Lookup("parent"); ok { 32 | continue 33 | } 34 | } 35 | 36 | fetch, _ := tag.Lookup("fetch") 37 | fetchConfigs := strings.Split(fetch, ",") 38 | for _, config := range fetchConfigs { 39 | switch config { 40 | case "user": 41 | if api == USER_API { 42 | preload = true 43 | } 44 | case "admin": 45 | if api == ADMIN_API { 46 | preload = true 47 | } 48 | case "eager": 49 | preload = true 50 | case "all": 51 | all = true 52 | case "parent": 53 | skip = skipParent 54 | default: 55 | fieldName = config 56 | } 57 | } 58 | if skip { 59 | preload = false 60 | } 61 | if preload { 62 | if all { 63 | db = db.Preload(fieldName, func(db *gorm.DB) *gorm.DB { 64 | return db.Unscoped() 65 | }) 66 | } else { 67 | db = db.Preload(fieldName) 68 | } 69 | } 70 | 71 | } 72 | return db 73 | } 74 | } 75 | 76 | func PageQueryResults(start, limit *uint) func(*gorm.DB) *gorm.DB { 77 | return func(db *gorm.DB) *gorm.DB { 78 | if start != nil && *start != 0 { 79 | db = db.Offset(*start) 80 | } 81 | if limit != nil && *limit != 0 { 82 | db = db.Limit(*limit) 83 | } 84 | return db 85 | } 86 | } 87 | 88 | func UpdateField(db *gorm.DB, item interface{}, field string, value interface{}) DefaultError { 89 | if item == nil { 90 | return NewServerError("Trying to update field on nil object") 91 | } 92 | 93 | if reflect.ValueOf(item).Kind() != reflect.Ptr { 94 | return NewServerError("Trying to update field on non-pointer object") 95 | } 96 | 97 | id, err := GetID(item) 98 | if err != nil { 99 | return NewServerError(err.Error()) 100 | } 101 | 102 | if id > 0 { 103 | err = db.Set("gorm:save_associations", false).Model(item).Update(field, value).Error 104 | if err != nil { 105 | return NewServerError(err.Error()) 106 | } 107 | return nil 108 | } 109 | 110 | return NewServerError("Trying to update field on empty object") 111 | } 112 | 113 | func UpdateFields(db *gorm.DB, item interface{}, transitive interface{}) DefaultError { 114 | if item == nil { 115 | return NewServerError("Trying to update fields on nil object") 116 | } 117 | 118 | if reflect.ValueOf(item).Kind() != reflect.Ptr { 119 | return NewServerError("Trying to update fields on non-pointer object") 120 | } 121 | 122 | id, err := GetID(item) 123 | if err != nil { 124 | return NewServerError(err.Error()) 125 | } 126 | 127 | if id > 0 { 128 | err = db.Set("gorm:save_associations", false).Model(item).Updates(transitive).Error 129 | if err != nil { 130 | return NewServerError(err.Error()) 131 | } 132 | return nil 133 | } 134 | 135 | return NewServerError("Trying to update fields on empty object") 136 | } 137 | 138 | func GetID(item interface{}) (uint, error) { 139 | v := reflect.ValueOf(item) 140 | if v.Kind() == reflect.Ptr { 141 | if v.IsNil() { 142 | return 0, errors.New("Cannot get value from nil item") 143 | } 144 | v = reflect.ValueOf(item).Elem() 145 | } 146 | f := v.FieldByName("ID") 147 | parentID := uint(f.Uint()) 148 | return parentID, nil 149 | } 150 | 151 | func TableNameFor(t reflect.Type) string { 152 | name := gorm.ToDBName(t.Name()) 153 | if strings.HasSuffix(name, "y") { 154 | name = name[:len(name)-1] + "ies" 155 | } else if strings.HasSuffix(name, "ss") { 156 | name = name + "es" 157 | } else { 158 | name = name + "s" 159 | } 160 | return name 161 | } 162 | -------------------------------------------------------------------------------- /core/error.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/brunoksato/golang-boilerplate/util" 7 | ) 8 | 9 | type DefaultError interface { 10 | Code() int 11 | Subcode() int 12 | Error() string 13 | Location() string 14 | Data() map[string]interface{} 15 | IsWarning() bool 16 | } 17 | 18 | const ERROR_CODE_WARNING int = 300 19 | const ERROR_CODE_BUSINESS_ERROR int = 400 20 | const ERROR_CODE_AUTHENTICATION_ERROR int = 401 21 | const ERROR_CODE_PERMISSION_ERROR int = 403 22 | const ERROR_CODE_NOT_FOUND int = 404 23 | const ERROR_CODE_SERVER_ERROR int = 500 24 | 25 | const ERROR_SUBCODE_UNDEFINED_IGNORE int = -1000 26 | const ERROR_SUBCODE_UNDEFINED int = -1999 27 | 28 | const ERROR_SUBCODE_FK int = -2000 29 | 30 | const ERROR_SUBCODE_CREDENTIALS_INVALID int = -2001 31 | 32 | const ERROR_SUBCODE_EMAIL int = -2002 33 | const ERROR_SUBCODE_NAME_TAKEN int = -2003 34 | const ERROR_SUBCODE_NAME_LENGTH int = -2004 35 | const ERROR_SUBCODE_NAME_FORMAT int = -2005 36 | const ERROR_SUBCODE_EMAIL_TAKEN int = -2006 37 | const ERROR_SUBCODE_EMAIL_FORMAT int = -2007 38 | const ERROR_SUBCODE_PASSWORD_LENGTH int = -2008 39 | const ERROR_SUBCODE_PASSWORD_FORMAT int = -2009 40 | const ERROR_SUBCODE_USERNAME_TAKEN int = -2010 41 | const ERROR_SUBCODE_USERNAME_LENGTH int = -2011 42 | const ERROR_SUBCODE_USERNAME_FORMAT int = -2012 43 | const ERROR_SUBCODE_PHONE_TAKEN int = -2013 44 | const ERROR_SUBCODE_PHONE_LENGTH int = -2014 45 | const ERROR_SUBCODE_PHONE_FORMAT int = -2015 46 | 47 | const ERROR_SUBCODE_USER_UNDERAGE int = -2800 48 | const ERROR_SUBCODE_USER_LACKS_PERMISSION int = -2801 49 | const ERROR_SUBCODE_OTHER_USER_LACKS_PERMISSION int = -2802 50 | 51 | const ERROR_SUBCODE_DATABASE_UNAVAILABLE int = -2900 52 | const ERROR_SUBCODE_SERVER_OVERLOADED int = -2910 53 | 54 | type CoreError struct { 55 | ErrCode int 56 | ErrSubcode int 57 | Message string 58 | ErrLocation string 59 | ErrData map[string]interface{} 60 | } 61 | 62 | func (err CoreError) Code() int { 63 | return err.ErrCode 64 | } 65 | 66 | func (err CoreError) Subcode() int { 67 | return err.ErrSubcode 68 | } 69 | 70 | func (err CoreError) Error() string { 71 | return err.Message 72 | } 73 | 74 | func (err CoreError) Location() string { 75 | return err.ErrLocation 76 | } 77 | 78 | func (err CoreError) Data() map[string]interface{} { 79 | return err.ErrData 80 | } 81 | 82 | func (err CoreError) IsWarning() bool { 83 | return err.ErrCode == ERROR_CODE_WARNING 84 | } 85 | 86 | func NewDefaultError(code, subcode int, location, msg string, data map[string]interface{}) DefaultError { 87 | if subcode == 0 { 88 | if code == ERROR_CODE_BUSINESS_ERROR { 89 | subcode = ERROR_SUBCODE_UNDEFINED 90 | } else { 91 | subcode = ERROR_SUBCODE_UNDEFINED_IGNORE 92 | } 93 | } 94 | 95 | err := CoreError{ 96 | ErrCode: code, 97 | ErrSubcode: subcode, 98 | Message: msg, 99 | ErrLocation: location, 100 | ErrData: data, 101 | } 102 | 103 | return err 104 | } 105 | 106 | func NewWarning(msg string, opts ...interface{}) DefaultError { 107 | return newElipsisError(ERROR_CODE_WARNING, msg, opts...) 108 | } 109 | 110 | func NewBusinessError(msg string, opts ...interface{}) DefaultError { 111 | return newElipsisError(ERROR_CODE_BUSINESS_ERROR, msg, opts...) 112 | } 113 | 114 | func NewAuthenticationError(msg string, opts ...interface{}) DefaultError { 115 | return newElipsisError(ERROR_CODE_AUTHENTICATION_ERROR, msg, opts...) 116 | } 117 | 118 | func NewPermissionError(msg string, opts ...interface{}) DefaultError { 119 | return newElipsisError(ERROR_CODE_PERMISSION_ERROR, msg, opts...) 120 | } 121 | 122 | func NewNotFoundError(msg string, opts ...interface{}) DefaultError { 123 | return newElipsisError(ERROR_CODE_NOT_FOUND, msg, opts...) 124 | } 125 | 126 | func NewServerError(msg string, opts ...interface{}) DefaultError { 127 | return newElipsisError(ERROR_CODE_SERVER_ERROR, msg, opts...) 128 | } 129 | 130 | func newElipsisError(code int, msg string, opts ...interface{}) DefaultError { 131 | subcode := 0 132 | data := map[string]interface{}{} 133 | 134 | for _, opt := range opts { 135 | switch reflect.TypeOf(opt).Kind() { 136 | case reflect.Int: 137 | subcode = opt.(int) 138 | case reflect.Map: 139 | data = opt.(map[string]interface{}) 140 | } 141 | } 142 | 143 | return NewDefaultError(code, subcode, util.ParentCallerInfo(), msg, data) 144 | } 145 | -------------------------------------------------------------------------------- /core/json.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | func ModelToJson(model interface{}) string { 11 | j, err := json.Marshal(model) 12 | if err != nil { 13 | panic(fmt.Sprintf("Error %v encoding JSON for %v", err, model)) 14 | } 15 | 16 | jsonStr := string(j) 17 | v := reflect.Indirect(reflect.ValueOf(model)) 18 | ot := v.Type() 19 | t := ot 20 | if t.Kind() == reflect.Array || t.Kind() == reflect.Slice { 21 | t = t.Elem() 22 | } else if t.Kind() == reflect.Interface { 23 | t = v.Elem().Type() 24 | } 25 | 26 | return jsonStr 27 | } 28 | 29 | func ModelToJsonMap(modl interface{}) map[string]interface{} { 30 | jsonStr := ModelToJson(modl) 31 | m := JsonToMap(jsonStr) 32 | return m 33 | } 34 | 35 | func JsonToMap(jsonStr string) map[string]interface{} { 36 | jsonMap := make(map[string]interface{}) 37 | 38 | err := json.Unmarshal([]byte(jsonStr), &jsonMap) 39 | if err != nil { 40 | panic(fmt.Sprintf("Error %v unmarshaling JSON for %v", err, jsonStr)) 41 | } 42 | 43 | return jsonMap 44 | } 45 | 46 | func JsonToMapArray(jsonStr string) []map[string]interface{} { 47 | var arr []map[string]interface{} 48 | err := json.Unmarshal([]byte(jsonStr), &arr) 49 | 50 | if err != nil { 51 | panic(fmt.Sprintf("Error %v unmarshaling JSON for %v", err, jsonStr)) 52 | } 53 | 54 | return arr 55 | } 56 | 57 | func JsonToModel(jsonStr string, item interface{}) error { 58 | err := json.Unmarshal([]byte(jsonStr), &item) 59 | 60 | if err == nil { 61 | v := reflect.Indirect(reflect.ValueOf(item)) 62 | ot := v.Type() 63 | t := ot 64 | if t.Kind() == reflect.Array || t.Kind() == reflect.Slice { 65 | t = t.Elem() 66 | } else if t.Kind() == reflect.Interface { 67 | t = v.Elem().Type() 68 | } 69 | } 70 | return err 71 | } 72 | 73 | func SetByJsonTag(item interface{}, jsonKey string, newVal interface{}) DefaultError { 74 | data := map[string]interface{}{ 75 | "type": reflect.TypeOf(item), 76 | "key": jsonKey, 77 | "val": newVal, 78 | } 79 | 80 | if jsonKey == "" || jsonKey == "-" { 81 | return NewBusinessError("Invalid JSON key", data) 82 | } 83 | 84 | v := reflect.ValueOf(item) 85 | if v.Kind() == reflect.Ptr { 86 | if v.IsNil() { 87 | return NewBusinessError("Cannot set value on nil item", data) 88 | } 89 | v = v.Elem() 90 | } 91 | t := v.Type() 92 | for i := 0; i < t.NumField(); i++ { 93 | f := t.Field(i) 94 | tag := f.Tag 95 | fKey := JsonName(f) 96 | vField := v.Field(i) 97 | if fKey == jsonKey { 98 | if tag.Get("settable") == "false" { 99 | return NewPermissionError("field is unsettable", data) 100 | } 101 | destType := vField.Type() 102 | if destType.Kind() == reflect.Ptr { 103 | destType = destType.Elem() 104 | } 105 | SetValue(vField.Addr(), destType, newVal) 106 | return nil 107 | } 108 | } 109 | 110 | return NewNotFoundError("field not found", data) 111 | } 112 | 113 | func GetFieldByJsonTag(item interface{}, jsonKey string) (field *reflect.StructField, merr DefaultError) { 114 | data := map[string]interface{}{ 115 | "type": reflect.TypeOf(item), 116 | "key": jsonKey, 117 | } 118 | 119 | if jsonKey == "" || jsonKey == "-" { 120 | return nil, NewBusinessError("Invalid JSON key", data) 121 | } 122 | 123 | v := reflect.ValueOf(item) 124 | if v.Kind() == reflect.Ptr { 125 | if v.IsNil() { 126 | return nil, NewBusinessError("Cannot set value on nil item", data) 127 | } 128 | v = reflect.ValueOf(item).Elem() 129 | } 130 | t := v.Type() 131 | for i := 0; i < t.NumField(); i++ { 132 | f := t.Field(i) 133 | fKey := JsonName(f) 134 | if fKey == jsonKey { 135 | return &f, nil 136 | } 137 | } 138 | 139 | return nil, NewNotFoundError("field not found", data) 140 | } 141 | 142 | func JsonName(f reflect.StructField) string { 143 | tag := f.Tag 144 | jsonTag := tag.Get("json") 145 | if jsonTag == "" { 146 | return "" 147 | } 148 | 149 | vals := strings.Split(jsonTag, ",") 150 | return vals[0] 151 | } 152 | 153 | func SetValue(v reflect.Value, destType reflect.Type, newVal interface{}) { 154 | vSet := v.Elem() 155 | 156 | if vSet.Kind() == reflect.Ptr { 157 | if newVal == nil { 158 | vSet.Set(reflect.Zero(vSet.Type())) 159 | return 160 | } else { 161 | floatVal, err := InterfaceToFloat64(newVal) 162 | if err == nil { 163 | switch destType.Kind() { 164 | case reflect.Int: 165 | n := int(floatVal) 166 | vSet.Set(reflect.ValueOf(&n)) 167 | case reflect.Int32: 168 | n := int32(floatVal) 169 | vSet.Set(reflect.ValueOf(&n)) 170 | case reflect.Int64: 171 | n := int64(floatVal) 172 | vSet.Set(reflect.ValueOf(&n)) 173 | case reflect.Uint: 174 | n := uint(floatVal) 175 | vSet.Set(reflect.ValueOf(&n)) 176 | case reflect.Uint8: 177 | n := uint8(floatVal) 178 | vSet.Set(reflect.ValueOf(&n)) 179 | case reflect.Uint16: 180 | n := uint16(floatVal) 181 | vSet.Set(reflect.ValueOf(&n)) 182 | case reflect.Uint32: 183 | n := uint32(floatVal) 184 | vSet.Set(reflect.ValueOf(&n)) 185 | case reflect.Uint64: 186 | n := uint64(floatVal) 187 | vSet.Set(reflect.ValueOf(&n)) 188 | case reflect.Float32: 189 | n := float32(floatVal) 190 | vSet.Set(reflect.ValueOf(&n)) 191 | case reflect.Float64: 192 | vSet.Set(reflect.ValueOf(&newVal)) 193 | case reflect.Struct: 194 | if destType == reflect.TypeOf(NullableTimestamp{}) { 195 | ts := NewNullableTimestamp(int64(floatVal)) 196 | vSet.Set(reflect.ValueOf(ts)) 197 | } else if destType == reflect.TypeOf(Timestamp{}) { 198 | ts := NewTimestamp(int64(floatVal)) 199 | vSet.Set(reflect.ValueOf(ts)) 200 | } 201 | default: 202 | vSet.Set(reflect.ValueOf(&floatVal)) 203 | } 204 | } else { 205 | if destType.Kind() == reflect.String { 206 | strVal := newVal.(string) 207 | vSet.Set(reflect.ValueOf(&strVal)) 208 | } else if destType.Kind() == reflect.Bool { 209 | boolVal := newVal.(bool) 210 | vSet.Set(reflect.ValueOf(&boolVal)) 211 | } else { 212 | vSet.Set(reflect.ValueOf(&newVal)) 213 | } 214 | } 215 | } 216 | } else { 217 | floatVal, err := InterfaceToFloat64(newVal) 218 | if err == nil { 219 | switch destType.Kind() { 220 | case reflect.Int, reflect.Int32, reflect.Int64: 221 | vSet.SetInt(int64(floatVal)) 222 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 223 | vSet.SetUint(uint64(floatVal)) 224 | case reflect.Float32, reflect.Float64: 225 | vSet.SetFloat(floatVal) 226 | default: 227 | vSet.Set(reflect.ValueOf(floatVal)) 228 | } 229 | } else { 230 | vSet.Set(reflect.ValueOf(newVal)) 231 | } 232 | } 233 | } 234 | 235 | func InterfaceToFloat64(i interface{}) (float64, error) { 236 | v := reflect.ValueOf(i) 237 | switch v.Kind() { 238 | case reflect.Int: 239 | return float64(i.(int)), nil 240 | case reflect.Uint: 241 | return float64(i.(uint)), nil 242 | case reflect.Uint64: 243 | return float64(i.(uint64)), nil 244 | case reflect.Int32: 245 | return float64(i.(int32)), nil 246 | case reflect.Int64: 247 | return float64(i.(int64)), nil 248 | case reflect.Float32: 249 | return float64(i.(float32)), nil 250 | case reflect.Float64: 251 | return i.(float64), nil 252 | default: 253 | return 0, fmt.Errorf("not implemented for type %v", v.Kind()) 254 | } 255 | } 256 | 257 | func IsJsonEnabled(f reflect.StructField, apiType APIType) bool { 258 | enabled := false 259 | 260 | tag := f.Tag 261 | jsonField := tag.Get("json") 262 | if jsonField != "" { 263 | sensitive := false 264 | matches := false 265 | vals := strings.Split(jsonField, ",") 266 | for j := 1; j < len(vals); j++ { 267 | val := vals[j] 268 | if val == "user" || val == "admin" { 269 | sensitive = true 270 | matches = matches || val == "user" && apiType == USER_API 271 | matches = matches || val == "admin" && apiType == ADMIN_API 272 | } 273 | } 274 | enabled = matches || !sensitive 275 | } 276 | 277 | return enabled 278 | } 279 | -------------------------------------------------------------------------------- /core/test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "net/url" 9 | "os" 10 | "reflect" 11 | "runtime" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/brunoksato/golang-boilerplate/util" 16 | "github.com/jinzhu/gorm" 17 | _ "github.com/jinzhu/gorm/dialects/postgres" 18 | "github.com/labstack/echo/v4" 19 | ) 20 | 21 | func OpenTestConnection() (db *gorm.DB, err error) { 22 | testDB := os.Getenv("SERVER_DB") 23 | if testDB == "" { 24 | testDB = "user=postgres dbname=server_test sslmode=disable" 25 | } 26 | db, err = gorm.Open("postgres", testDB) 27 | return 28 | } 29 | 30 | // Rest Test 31 | func typeTestHandler(c echo.Context) { 32 | parent := expectedTypeName(c.Get("ParentType").(reflect.Type)) 33 | child := expectedTypeName(c.Get("Type").(reflect.Type)) 34 | 35 | fmt.Fprintf(c.Response(), "Parent Type: %s, Type: %s", parent, child) 36 | } 37 | 38 | func expectedTypeName(t reflect.Type) (expected string) { 39 | if t == nil { 40 | expected = "nil" 41 | } else { 42 | expected = t.Name() 43 | } 44 | return 45 | } 46 | 47 | func NewTestRequest(method, path string) (*httptest.ResponseRecorder, *http.Request) { 48 | request, err := http.NewRequest(method, path, nil) 49 | request.Header.Set("X-Company", "Office") 50 | request.Header.Add("Content-Type", "application/json") 51 | if err != nil { 52 | fmt.Println("Error configuring test request:", err) 53 | } 54 | recorder := httptest.NewRecorder() 55 | 56 | return recorder, request 57 | } 58 | 59 | func NewTestPost(method, path string, body interface{}) (*httptest.ResponseRecorder, *http.Request) { 60 | jsonStr := ModelToJson(body) 61 | bodyStr := bytes.NewBufferString(jsonStr) 62 | request, err := http.NewRequest(method, path, bodyStr) 63 | request.Header.Set("X-Company", "Office") 64 | request.Header.Add("Content-Type", "application/json") 65 | if err != nil { 66 | fmt.Println("Error configuring test request:", err) 67 | } 68 | recorder := httptest.NewRecorder() 69 | 70 | return recorder, request 71 | } 72 | 73 | func NewTestForm(path string, params map[string]interface{}) (*httptest.ResponseRecorder, *http.Request) { 74 | values := url.Values{} 75 | for k, v := range params { 76 | values.Set(k, fmt.Sprintf("%v", v)) 77 | } 78 | 79 | // Can't seem to get this working. So, for now... 80 | querypath := fmt.Sprintf("%s?%s", path, values.Encode()) 81 | recorder, request := NewTestRequest("POST", querypath) 82 | 83 | return recorder, request 84 | } 85 | 86 | // Assert methods 87 | 88 | func AssertResponse(t *testing.T, rr *httptest.ResponseRecorder, body string, code int) { 89 | if gotBody := strings.TrimSpace(string(rr.Body.Bytes())); body != gotBody { 90 | t.Errorf("assertResponse: expected body to be %s but got %s. (caller: %s)", body, gotBody, util.CallerInfo()) 91 | } 92 | AssertResponseCode(t, rr, code) 93 | } 94 | 95 | func AssertResponseCode(t *testing.T, rr *httptest.ResponseRecorder, code int) { 96 | if code != rr.Code { 97 | t.Errorf("assertResponse: expected code to be %d but got %d. (caller: %s)", code, rr.Code, util.CallerInfo()) 98 | } 99 | } 100 | 101 | func AssertEqual(t *testing.T, expected, actual interface{}) { 102 | if !reflect.DeepEqual(expected, actual) { 103 | kind := reflect.ValueOf(expected).Kind() 104 | if kind == reflect.Map { 105 | if mExp, ok := expected.(map[string]interface{}); ok { 106 | mAct := actual.(map[string]interface{}) 107 | for key, value := range mExp { 108 | AssertEqual(t, value, mAct[key]) 109 | } 110 | } else if mExp, ok := expected.(map[float32][]uint); ok { 111 | mAct := actual.(map[float32][]uint) 112 | for key, value := range mExp { 113 | AssertEqual(t, value, mAct[key]) 114 | } 115 | } else { 116 | mExp := expected.(map[uint]uint) 117 | mAct := actual.(map[uint]uint) 118 | for key, value := range mExp { 119 | AssertEqual(t, value, mAct[key]) 120 | } 121 | } 122 | } else if kind == reflect.Array || kind == reflect.Slice { 123 | eVal := reflect.ValueOf(expected) 124 | aVal := reflect.ValueOf(actual) 125 | AssertEqual(t, eVal.Len(), aVal.Len()) 126 | for i := 0; i < aVal.Len(); i++ { 127 | AssertEqual(t, eVal.Index(i).Interface(), aVal.Index(i).Interface()) 128 | } 129 | } 130 | } 131 | } 132 | 133 | func AssertNil(t *testing.T, actual interface{}) { 134 | if actual != nil && !reflect.ValueOf(actual).IsNil() { 135 | t.Errorf("assertNil: Expected nil value, but got:\n%v\n (caller: %s)", actual, CallerInfo()) 136 | } 137 | } 138 | func AssertNotNil(t *testing.T, actual interface{}) { 139 | if actual == nil || reflect.ValueOf(actual).IsNil() { 140 | t.Errorf("assertNotNil: Expected object to not be nil (caller: %s)", CallerInfo()) 141 | } 142 | } 143 | func AssertZeroStruct(t *testing.T, actual interface{}) { 144 | if !util.IsZeroStruct(actual) { 145 | t.Errorf("assertZeroStruct: Expected zero struct, but got:\n%v\n (caller: %s)", actual, CallerInfo()) 146 | } 147 | } 148 | func AssertFalse(t *testing.T, result bool) { 149 | if result { 150 | t.Errorf("assertFalse: Assertion is true (caller: %s)", CallerInfo()) 151 | } 152 | } 153 | func AssertTrue(t *testing.T, result bool) { 154 | if !result { 155 | t.Errorf("assertTrue: Assertion is false (caller: %s)", CallerInfo()) 156 | } 157 | } 158 | 159 | func AssertNoError(t *testing.T, err error) { 160 | if err != nil { 161 | t.Errorf("assertNoError: Expected no error, but received message: %s (caller: %s)", err, CallerInfo()) 162 | } 163 | } 164 | 165 | func AssertWarning(t *testing.T, msg string, err DefaultError) { 166 | AssertDefaultError(t, "Warning", msg, err, CallerInfo()) 167 | } 168 | 169 | func AssertBusinessError(t *testing.T, msg string, err DefaultError) { 170 | AssertDefaultError(t, "Business Error", msg, err, CallerInfo()) 171 | } 172 | 173 | func AssertPermissionError(t *testing.T, msg string, err DefaultError) { 174 | AssertDefaultError(t, "Permission Error", msg, err, CallerInfo()) 175 | } 176 | 177 | func AssertNotFoundError(t *testing.T, msg string, err DefaultError) { 178 | AssertDefaultError(t, "Not Found Error", msg, err, CallerInfo()) 179 | } 180 | 181 | func AssertServerError(t *testing.T, msg string, err DefaultError) { 182 | AssertDefaultError(t, "Server Error", msg, err, CallerInfo()) 183 | } 184 | 185 | func AssertDefaultError(t *testing.T, prefix, msg string, err DefaultError, callerInfo string) { 186 | if err == nil { 187 | t.Errorf("AssertDefaultError: Expected an error, but it was nil (caller: %s)", callerInfo) 188 | return 189 | } 190 | 191 | if _, ok := err.(DefaultError); !ok { 192 | t.Errorf("AssertDefaultError: Expected a petmondo error, but got: %s (caller: %s)", err, callerInfo) 193 | return 194 | } 195 | 196 | errMsg := err.Error() 197 | 198 | idx := strings.Index(errMsg, "(caller:") 199 | if idx != -1 { 200 | errMsg = errMsg[:idx-1] 201 | } 202 | 203 | if msg != errMsg { 204 | t.Errorf("AssertDefaultError: Expected error \"%s\", but got \"%s\" (caller: %s)", msg, errMsg, callerInfo) 205 | return 206 | } 207 | } 208 | 209 | func LineInfo() string { 210 | _, file, line, ok := runtime.Caller(1) 211 | if !ok { 212 | return "" 213 | } 214 | parts := strings.Split(file, "/") 215 | file = parts[len(parts)-1] 216 | return fmt.Sprintf("%s:%d", file, line) 217 | } 218 | 219 | func CallerInfo() string { 220 | _, file, line, ok := runtime.Caller(2) 221 | if !ok { 222 | return "" 223 | } 224 | parts := strings.Split(file, "/") 225 | file = parts[len(parts)-1] 226 | return fmt.Sprintf("%s:%d", file, line) 227 | } 228 | 229 | func ParentCallerInfo() string { 230 | _, file, line, ok := runtime.Caller(3) 231 | if !ok { 232 | return "" 233 | } 234 | parts := strings.Split(file, "/") 235 | file = parts[len(parts)-1] 236 | return fmt.Sprintf("%s:%d", file, line) 237 | } 238 | -------------------------------------------------------------------------------- /core/timestamp.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/brunoksato/golang-boilerplate/util" 10 | ) 11 | 12 | type Timestamp struct { 13 | Time time.Time 14 | } 15 | 16 | // Value implements the driver Valuer interface. 17 | func (ts Timestamp) Value() (driver.Value, error) { 18 | return ts.Time, nil 19 | } 20 | 21 | func (ts *Timestamp) Scan(value interface{}) error { 22 | ts.Time = value.(time.Time) 23 | return nil 24 | } 25 | 26 | func (ts Timestamp) MarshalJSON() ([]byte, error) { 27 | return json.Marshal(int64(ts.Timestamp())) 28 | } 29 | 30 | func (ts *Timestamp) UnmarshalJSON(data []byte) error { 31 | tsInt, err := strconv.Atoi(string(data)) 32 | if err != nil { 33 | return err 34 | } 35 | ts.SetTimestamp(int64(tsInt)) 36 | return nil 37 | } 38 | 39 | func (ts Timestamp) Timestamp() int64 { 40 | return util.NanoToMicro(ts.Time.UnixNano()) 41 | } 42 | 43 | func (ts *Timestamp) SetTimestamp(val int64) { 44 | ts.Time = time.Unix(0, util.MicroToNano(val)) 45 | } 46 | 47 | func NewTimestamp(val int64) Timestamp { 48 | t := time.Unix(0, util.MicroToNano(val)) 49 | return Timestamp{Time: t} 50 | } 51 | 52 | type NullableTimestamp struct { 53 | Time time.Time 54 | } 55 | 56 | // Value implements the driver Valuer interface. 57 | func (ts *NullableTimestamp) Value() (driver.Value, error) { 58 | if ts == nil { 59 | return nil, nil 60 | } 61 | 62 | return ts.Time, nil 63 | } 64 | 65 | func (ts *NullableTimestamp) Scan(value interface{}) error { 66 | if ts == nil { 67 | return nil 68 | } else { 69 | if value != nil { 70 | t := value.(time.Time) 71 | ts.Time = t 72 | } 73 | } 74 | return nil 75 | } 76 | 77 | func (ts *NullableTimestamp) MarshalJSON() ([]byte, error) { 78 | if ts == nil { 79 | return []byte("null"), nil 80 | } 81 | 82 | return json.Marshal(int64(ts.Timestamp())) 83 | } 84 | 85 | func (ts *NullableTimestamp) UnmarshalJSON(data []byte) error { 86 | if ts == nil { 87 | return nil 88 | } else { 89 | if len(data) > 0 { 90 | tsInt, err := strconv.Atoi(string(data)) 91 | if err != nil { 92 | return err 93 | } 94 | ts.SetTimestamp(int64(tsInt)) 95 | } 96 | } 97 | return nil 98 | } 99 | 100 | func (ts NullableTimestamp) Timestamp() int64 { 101 | return util.NanoToMicro(ts.Time.UnixNano()) 102 | } 103 | 104 | func (ts *NullableTimestamp) SetTimestamp(val int64) { 105 | ts.Time = time.Unix(0, util.MicroToNano(val)) 106 | } 107 | 108 | func NewNullableTimestamp(val int64) *NullableTimestamp { 109 | t := time.Unix(0, util.MicroToNano(val)) 110 | return &NullableTimestamp{Time: t} 111 | } 112 | -------------------------------------------------------------------------------- /core/type.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | type APIType string 4 | 5 | const USER_API APIType = "USER_API" 6 | const CRONJOB_API APIType = "CRONJOB_API" 7 | const ADMIN_API APIType = "ADMIN_API" 8 | -------------------------------------------------------------------------------- /db/dbconf.yml: -------------------------------------------------------------------------------- 1 | development: 2 | driver: postgres 3 | open: dbname=server sslmode=disable 4 | 5 | staging: 6 | driver: postgres 7 | open: host=you_host user=you_user dbname=you_db password=you_password 8 | 9 | production: 10 | driver: postgres 11 | open: host=you_host user=you_user dbname=you_db password=you_password 12 | 13 | test: 14 | driver: postgres 15 | open: user=postgres dbname=server_test sslmode=disable 16 | 17 | #open: $DATABASE_URL 18 | -------------------------------------------------------------------------------- /db/migrations/20190611174624_base.sql: -------------------------------------------------------------------------------- 1 | 2 | -- +goose Up 3 | -- SQL in section 'Up' is executed when this migration is applied 4 | 5 | 6 | create table configurations( 7 | id serial not null, 8 | created_at timestamp with time zone DEFAULT now(), 9 | updated_at timestamp with time zone DEFAULT now(), 10 | min_value_buy numeric(12,2), 11 | ); 12 | ALTER TABLE ONLY configurations ADD CONSTRAINT configurations_pkey PRIMARY KEY (id); 13 | 14 | CREATE TABLE users( 15 | id serial not null, 16 | created_at timestamp with time zone DEFAULT now(), 17 | updated_at timestamp with time zone DEFAULT now(), 18 | deleted_at timestamp with time zone, 19 | last_login timestamp with time zone, 20 | hashed_password bytea NOT NULL, 21 | password varchar(255), 22 | image varchar(255), 23 | phone varchar(255), 24 | name varchar(255) not null, 25 | username varchar(50) not null, 26 | email varchar(100) not null, 27 | balance numeric(12,2) default 0, 28 | admin boolean not null default false, 29 | ban boolean not null default false, 30 | ); 31 | 32 | ALTER TABLE ONLY users ADD CONSTRAINT users_pkey PRIMARY KEY (id); 33 | CREATE UNIQUE INDEX idx_users_email ON users USING btree (email); 34 | CREATE UNIQUE INDEX idx_users_username ON users USING btree (username); 35 | CREATE UNIQUE INDEX idx_lower_case_username ON users ((lower(username))); 36 | 37 | 38 | -- +goose Down 39 | -- SQL section 'Down' is executed when this migration is rolled back 40 | 41 | DROP TABLE configurations; 42 | DROP TABLE users; -------------------------------------------------------------------------------- /db/seed.sql: -------------------------------------------------------------------------------- 1 | insert into configurations (min_value_buy) values (1); 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/brunoksato/golang-boilerplate 2 | 3 | // +heroku goVersion go1.12.5 4 | require ( 5 | github.com/Sirupsen/logrus v0.0.0-20170713114250-a3f95b5c4235 6 | github.com/Zauberstuhl/go-coinbase v1.0.0 7 | github.com/asaskevich/govalidator v0.0.0-20170425121227-4918b99a7cb9 8 | github.com/brunoksato/argon-server v0.0.0-20190812163325-5cd96a6e3814 // indirect 9 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 10 | github.com/heroku/x v0.0.1 11 | github.com/jinzhu/gorm v1.9.2 12 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a 13 | github.com/labstack/echo/v4 v4.1.6 14 | github.com/labstack/gommon v0.2.9 15 | github.com/lib/pq v1.0.0 16 | github.com/mailru/easyjson v0.0.0-20180323154445-8b799c424f57 17 | github.com/mattn/go-colorable v0.1.2 18 | github.com/mattn/go-isatty v0.0.8 19 | github.com/mitchellh/mapstructure v1.1.2 20 | github.com/olivere/elastic v6.2.6+incompatible 21 | github.com/pkg/errors v0.8.1 22 | github.com/satori/go.uuid v1.1.0 23 | github.com/sendgrid/sendgrid-go v3.5.0+incompatible 24 | github.com/stretchr/testify v1.3.0 25 | github.com/valyala/bytebufferpool v1.0.0 26 | github.com/valyala/fasttemplate v1.0.1 27 | go.elastic.co/apm/module/apmechov4 v1.4.0 28 | go.elastic.co/apm/module/apmgorm v1.4.0 29 | go4.org v0.0.0-20180417224846-9599cf28b011 30 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 31 | golang.org/x/net v0.0.0-20190607181551-461777fb6f67 32 | golang.org/x/sys v0.0.0-20190609082536-301114b31cce 33 | golang.org/x/text v0.3.2 34 | ) 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU= 4 | cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= 5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 6 | github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20181009230506-ac834ce67862/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo= 7 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= 8 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 9 | github.com/Sirupsen/logrus v0.0.0-20170713114250-a3f95b5c4235 h1:6mZrwf2LAPUUlN1mYu4bLKPNTgTQoSMoslMjJrrRl1k= 10 | github.com/Sirupsen/logrus v0.0.0-20170713114250-a3f95b5c4235/go.mod h1:rmk17hk6i8ZSAJkSDa7nOxamrG+SP4P0mm+DAvExv4U= 11 | github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= 12 | github.com/Zauberstuhl/go-coinbase v1.0.0 h1:P98MLwPHd+Af/odAwM7zo4Xm9zA0A3+kIb7curHAMIA= 13 | github.com/Zauberstuhl/go-coinbase v1.0.0/go.mod h1:8ZnAd21UXoShhsswjy9qIwsRhCOHJazaAK/IpGBQG0w= 14 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 15 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 16 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 17 | github.com/armon/go-proxyproto v0.0.0-20190211145416-68259f75880e/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= 18 | github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= 19 | github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 20 | github.com/asaskevich/govalidator v0.0.0-20170425121227-4918b99a7cb9 h1:IwoI5FDkxVBZLw5UtX8KBKa2mW2zCKdGPfdyBx6nr9U= 21 | github.com/asaskevich/govalidator v0.0.0-20170425121227-4918b99a7cb9/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 22 | github.com/aws/aws-sdk-go v1.13.10/go.mod h1:ZRmQr0FajVIyZ4ZzBYKG5P3ZqPz9IHG41ZoMu1ADI3k= 23 | github.com/axiomhq/hyperloglog v0.0.0-20180317131949-fe9507de0228/go.mod h1:IOXAcuKIFq/mDyuQ4wyJuJ79XLMsmLM+5RdQ+vWrL7o= 24 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 25 | github.com/brunoksato/argon-server v0.0.0-20190812163325-5cd96a6e3814 h1:XyNeV5zRlxDl6788jjYhuq3bDiXyJ9an6O0DeEEaqBc= 26 | github.com/brunoksato/argon-server v0.0.0-20190812163325-5cd96a6e3814/go.mod h1:VrxOjGMUZtouIBNLEsX7sh4Rw95oqX0G46Dmrk/v0Ss= 27 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 28 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 29 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 31 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 32 | github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc= 33 | github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgfR0jcVVXuTsYv/erY2NnEDqwRojbxR1rBYA= 34 | github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= 35 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 36 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 37 | github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= 38 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 39 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 40 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 41 | github.com/elastic/go-sysinfo v0.0.0-20190103140604-e68552284485 h1:k9Ac5c19ZDF7XOktjJP50LTn3a9+HPUONWXyqT6Xt7M= 42 | github.com/elastic/go-sysinfo v0.0.0-20190103140604-e68552284485/go.mod h1:5kYRMF9nitZzZ4odJqSHV1DddYPTJ+QB4YsyWmNyJzA= 43 | github.com/elastic/go-windows v0.0.0-20180831131045-bb1581babc04/go.mod h1:jgPEIvw0E137UFC4zfkcjyM9T9shDL+JIfqFXQQhVwc= 44 | github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= 45 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 46 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 47 | github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= 48 | github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= 49 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 50 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 51 | github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= 52 | github.com/go-ini/ini v1.33.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 53 | github.com/go-kit/kit v0.6.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 54 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 55 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 56 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 57 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 58 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 59 | github.com/gofrs/uuid v3.1.0+incompatible h1:q2rtkjaKT4YEr6E1kamy0Ha4RtepWlQBedyHx0uzKwA= 60 | github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 61 | github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= 62 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 63 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 64 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 65 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 66 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 67 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 68 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 69 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 70 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 71 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 72 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 73 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 74 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 75 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 76 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 77 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 78 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 79 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 80 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 81 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 82 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 83 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 84 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 85 | github.com/heroku/x v0.0.1 h1:SF/19OoRU28VbFnk7PE4ufX8zbqMWvnm3zkTtlsB3M0= 86 | github.com/heroku/x v0.0.1/go.mod h1:DMrW71ANm+83z4O90qbXedG3ZeRnOj/thqjFWSnTZZQ= 87 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 88 | github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= 89 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 90 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 91 | github.com/jinzhu/gorm v0.0.0-20181007004937-742154be9a26 h1:g3MQZVKtYijVx47x2BQdiH+ZSTAwIrvqLUDijmns5o8= 92 | github.com/jinzhu/gorm v0.0.0-20181007004937-742154be9a26/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= 93 | github.com/jinzhu/gorm v1.9.2 h1:lCvgEaqe/HVE+tjAR2mt4HbbHAZsQOv3XAZiEZV37iw= 94 | github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= 95 | github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc= 96 | github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 97 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k= 98 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 99 | github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc= 100 | github.com/jinzhu/now v1.0.0 h1:6WV8LvwPpDhKjo5U9O6b4+xdG/jTXNPwlDme/MTo8Ns= 101 | github.com/jinzhu/now v1.0.0/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc= 102 | github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 103 | github.com/joeshaw/envdecode v0.0.0-20180129163420-d5f34bca07f3/go.mod h1:Q+alOFAXgW5SrcfMPt/G4B2oN+qEcQRJjkn/f4mKL04= 104 | github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4= 105 | github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= 106 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 107 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 108 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 109 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 110 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 111 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 112 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 113 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 114 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 115 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 116 | github.com/labstack/echo v0.0.0-20180911044237-1abaa3049251 h1:4q++nZ4OEtmbHazhA/7i3T9B+CBWtnHpuMMcW55ZjRk= 117 | github.com/labstack/echo v0.0.0-20180911044237-1abaa3049251/go.mod h1:rWD2DNQgFb1IY9lVYZVLWn2Ko4dyHZ/LpHORyBLP3hI= 118 | github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= 119 | github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= 120 | github.com/labstack/echo/v4 v0.0.0-20180911044237-1abaa3049251 h1:4q++nZ4OEtmbHazhA/7i3T9B+CBWtnHpuMMcW55ZjRk= 121 | github.com/labstack/echo/v4 v0.0.0-20180911044237-1abaa3049251/go.mod h1:rWD2DNQgFb1IY9lVYZVLWn2Ko4dyHZ/LpHORyBLP3hI= 122 | github.com/labstack/echo/v4 v4.0.0/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno= 123 | github.com/labstack/echo/v4 v4.1.6 h1:WOvLa4T1KzWCRpANwz0HGgWDelXSSGwIKtKBbFdHTv4= 124 | github.com/labstack/echo/v4 v4.1.6/go.mod h1:kU/7PwzgNxZH4das4XNsSpBSOD09XIF5YEPzjpkGnGE= 125 | github.com/labstack/gommon v0.0.0-20180312174116-6fe1405d73ec/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= 126 | github.com/labstack/gommon v0.0.0-20180506140623-0a22a0df01a7 h1:zBqzrh1EkrO1zj/pDGT+UrB1M1Ihzqjc0K9MOynW2tI= 127 | github.com/labstack/gommon v0.0.0-20180506140623-0a22a0df01a7/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= 128 | github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= 129 | github.com/labstack/gommon v0.2.9 h1:heVeuAYtevIQVYkGj6A41dtfT91LrvFG220lavpWhrU= 130 | github.com/labstack/gommon v0.2.9/go.mod h1:E8ZTmW9vw5az5/ZyHWCp0Lw4OH2ecsaBP1C/NKavGG4= 131 | github.com/leesper/go_rng v0.0.0-20171009123644-5344a9259b21/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U= 132 | github.com/lib/pq v0.0.0-20160511035104-ee1442bda7bd h1:4boQFkBA2FViSz6B5dPK4yt+Ur9UKHvjtpoI4CrkrlM= 133 | github.com/lib/pq v0.0.0-20160511035104-ee1442bda7bd/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 134 | github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= 135 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 136 | github.com/lstoll/grpce v1.7.0/go.mod h1:XiCWl3R+avNCT7KsTjv3qCblgsSqd0SC4ymySrH226g= 137 | github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= 138 | github.com/mailru/easyjson v0.0.0-20180323154445-8b799c424f57 h1:qhv1ir3dIyOFmFU+5KqG4dF3zSQTA4nn1DFhu2NQC44= 139 | github.com/mailru/easyjson v0.0.0-20180323154445-8b799c424f57/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 140 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 141 | github.com/mattn/go-colorable v0.1.0 h1:v2XXALHHh6zHfYTJ+cSkwtyffnaOyR1MXaA91mTrb8o= 142 | github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 143 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 144 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 145 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 146 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= 147 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 148 | github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= 149 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 150 | github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= 151 | github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 152 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 153 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 154 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 155 | github.com/mwitkow/go-grpc-middleware v1.0.0/go.mod h1:wqm8af53+/cILryTaG+dCJS6CsDMVZDxlKh6lSkF19U= 156 | github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= 157 | github.com/olivere/elastic v6.2.6+incompatible h1:DABh8qMNCjk+X7O4nbX6A7GA8Aa7K6Z98U8rnUlq81Q= 158 | github.com/olivere/elastic v6.2.6+incompatible/go.mod h1:J+q1zQJTgAz9woqsbVRqGeB5G1iqDKVBWLNSYW8yfJ8= 159 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 160 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 161 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 162 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= 163 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 164 | github.com/pkg/errors v0.0.0-20180311214515-816c9085562c h1:F5RoIh7F9wB47PvXvpP1+Ihq1TkyC8iRdvwfKkESEZQ= 165 | github.com/pkg/errors v0.0.0-20180311214515-816c9085562c/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 166 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 167 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 168 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 169 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 170 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 171 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 172 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 173 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= 174 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 175 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 176 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 177 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 178 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 179 | github.com/rcrowley/go-metrics v0.0.0-20160613154715-cfa5a85e9f0a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 180 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 181 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 182 | github.com/santhosh-tekuri/jsonschema v1.2.3/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= 183 | github.com/satori/go.uuid v1.1.0 h1:B9KXyj+GzIpJbV7gmr873NsY6zpbxNy24CBtGrk7jHo= 184 | github.com/satori/go.uuid v1.1.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 185 | github.com/sendgrid/rest v2.4.1+incompatible h1:HDib/5xzQREPq34lN3YMhQtMkdXxS/qLp5G3k9a5++4= 186 | github.com/sendgrid/rest v2.4.1+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= 187 | github.com/sendgrid/sendgrid-go v3.5.0+incompatible h1:kosbgHyNVYVaqECDYvFVLVD9nvThweBd6xp7vaCT3GI= 188 | github.com/sendgrid/sendgrid-go v3.5.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= 189 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 190 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 191 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 192 | github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 193 | github.com/soveran/redisurl v0.0.0-20180322091936-eb325bc7a4b8/go.mod h1:FVJ8jbHu7QrNFs3bZEsv/L5JjearIAY9N0oXh2wk+6Y= 194 | github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 195 | github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 196 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 197 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 198 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 199 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 200 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 201 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 202 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 203 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 204 | github.com/valyala/bytebufferpool v0.0.0-20160817181652-e746df99fe4a/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 205 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 206 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 207 | github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8= 208 | github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw= 209 | github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= 210 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 211 | go.elastic.co/apm v1.4.0 h1:5/C4gtQlU6dZISUPHYG45yq6yk5ELIeEu6MwUL529Q4= 212 | go.elastic.co/apm v1.4.0/go.mod h1:Yr6TY/W+k8/YkTXvHcDPde3B7Y983r95gbY54HuLTdU= 213 | go.elastic.co/apm/module/apmecho v1.4.0 h1:gSS5KilHrR4XosxMBeftJcsOQXqAzqyQohf0E5+5OqM= 214 | go.elastic.co/apm/module/apmecho v1.4.0/go.mod h1:NjvTvVQIhv9z4O5OR2WC8rCOMPj+wznd7ig5HkNzqlg= 215 | go.elastic.co/apm/module/apmechov4 v1.4.0 h1:X+ML3nCh4NcQ5jS1dcQTJa89hqGnHUlvZ9TgWC8Gp5Q= 216 | go.elastic.co/apm/module/apmechov4 v1.4.0/go.mod h1:6hdICexYpQ31WnZy4HG2wEGygOc2HXq1UffvF1rtxj8= 217 | go.elastic.co/apm/module/apmgorm v1.4.0 h1:WfH/eNLZO28O3GF4i2Cfm9h0HIgAvkx20nvOfvSYVaQ= 218 | go.elastic.co/apm/module/apmgorm v1.4.0/go.mod h1:GbnZw7FqwV5tpsN+eo0Js2oPIMr+iLWLge+ifwOC8tM= 219 | go.elastic.co/apm/module/apmhttp v1.4.0 h1:gHqWQ+87ySV6XZrCqGdbQtd8pRdE8CRjod3VSx//3y8= 220 | go.elastic.co/apm/module/apmhttp v1.4.0/go.mod h1:GUANFA9BvYsfn3DRj7x9oQXKVsqKrZO2OYd/J+wIzRY= 221 | go.elastic.co/apm/module/apmsql v1.4.0 h1:iDXZu/FvyOGKhG9MjlFtGy6++FxlWcwzmTVfec8dV8s= 222 | go.elastic.co/apm/module/apmsql v1.4.0/go.mod h1:bWMD5BOyuhoTKnDnMa2/StJbUPvRD4Ff6UF5JWcZ40Y= 223 | go.elastic.co/fastjson v1.0.0 h1:ooXV/ABvf+tBul26jcVViPT3sBir0PvXgibYB1IQQzg= 224 | go.elastic.co/fastjson v1.0.0/go.mod h1:PmeUOMMtLHQr9ZS9J9owrAVg0FkaZDRZJEFTTGHtchs= 225 | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 226 | go4.org v0.0.0-20180417224846-9599cf28b011/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= 227 | golang.org/x/crypto v0.0.0-20180312195533-182114d58262 h1:1NLVUmR8SQ7cNNA5Vo7ronpXbR+5A+9IwIC/bLE7D8Y= 228 | golang.org/x/crypto v0.0.0-20180312195533-182114d58262/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 229 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 230 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 231 | golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 232 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 233 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= 234 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 235 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= 236 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 237 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 238 | golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 239 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 240 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 241 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 242 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 243 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 244 | golang.org/x/net v0.0.0-20160805163904-075e191f1818/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 245 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 246 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 247 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 248 | golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 249 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 250 | golang.org/x/net v0.0.0-20181213202711-891ebc4b82d6/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 251 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 252 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 253 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 254 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 255 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 256 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 257 | golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 258 | golang.org/x/net v0.0.0-20190607181551-461777fb6f67 h1:rJJxsykSlULwd2P2+pg/rtnwN2FrWp4IuCxOSyS0V00= 259 | golang.org/x/net v0.0.0-20190607181551-461777fb6f67/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 260 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 261 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 262 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 263 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 264 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 265 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 266 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 267 | golang.org/x/sys v0.0.0-20170615053224-fb4cac33e319/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 268 | golang.org/x/sys v0.0.0-20180312081825-c28acc882ebc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 269 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 270 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 271 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 272 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 273 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 274 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 275 | golang.org/x/sys v0.0.0-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 276 | golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 277 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 278 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 279 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 280 | golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 281 | golang.org/x/sys v0.0.0-20190609082536-301114b31cce h1:CQakrGkKbydnUmt7cFIlmQ4lNQiqdTPt6xzXij4nYCc= 282 | golang.org/x/sys v0.0.0-20190609082536-301114b31cce/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 283 | golang.org/x/text v0.0.0-20180511172408-5c1cf69b5978/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 284 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 285 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 286 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 287 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 288 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 289 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 290 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 291 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 292 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 293 | golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 294 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 295 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 296 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 297 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 298 | golang.org/x/tools v0.0.0-20190608022120-eacb66d2a7c3/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 299 | gonum.org/v1/gonum v0.0.0-20190502212712-4a2eb0188cbc/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= 300 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= 301 | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= 302 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 303 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 304 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 305 | google.golang.org/genproto v0.0.0-20181221175505-bd9b4fb69e2f/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 306 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 307 | google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 308 | google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 309 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 310 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 311 | google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= 312 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 313 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 314 | gopkg.in/caio/go-tdigest.v2 v2.3.0/go.mod h1:HPfh/CLN8UWDMOC76lqxVeKa5E24ypoVuTj4BLMb9cU= 315 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 316 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 317 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 318 | gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 319 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 320 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 321 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 322 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 323 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 324 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 325 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 326 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 327 | howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M= 328 | howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= 329 | -------------------------------------------------------------------------------- /log/hook.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/Sirupsen/logrus" 10 | "github.com/olivere/elastic" 11 | ) 12 | 13 | var ( 14 | // Fired if the 15 | // index is not created 16 | ErrCannotCreateIndex = fmt.Errorf("Cannot create index") 17 | ) 18 | 19 | type IndexNameFunc func() string 20 | 21 | // ElasticHook is a logrus 22 | // hook for ElasticSearch 23 | type ElasticHook struct { 24 | client *elastic.Client 25 | host string 26 | index IndexNameFunc 27 | levels []logrus.Level 28 | ctx context.Context 29 | ctxCancel context.CancelFunc 30 | } 31 | 32 | // NewElasticHook creates new hook 33 | // client - ElasticSearch client using gopkg.in/olivere/elastic.v5 34 | // host - host of system 35 | // level - log level 36 | // index - name of the index in ElasticSearch 37 | func NewElasticHook(client *elastic.Client, host string, level logrus.Level, index string) (*ElasticHook, error) { 38 | return NewElasticHookWithFunc(client, host, level, func() string { return index }) 39 | } 40 | 41 | // NewElasticHookWithFunc creates new hook with 42 | // function that provides the index name. This is useful if the index name is 43 | // somehow dynamic especially based on time. 44 | // client - ElasticSearch client using gopkg.in/olivere/elastic.v5 45 | // host - host of system 46 | // level - log level 47 | // indexFunc - function providing the name of index 48 | func NewElasticHookWithFunc(client *elastic.Client, host string, level logrus.Level, indexFunc IndexNameFunc) (*ElasticHook, error) { 49 | levels := []logrus.Level{} 50 | for _, l := range []logrus.Level{ 51 | logrus.PanicLevel, 52 | logrus.FatalLevel, 53 | logrus.ErrorLevel, 54 | logrus.WarnLevel, 55 | logrus.InfoLevel, 56 | logrus.DebugLevel, 57 | } { 58 | if l <= level { 59 | levels = append(levels, l) 60 | } 61 | } 62 | 63 | ctx, cancel := context.WithCancel(context.TODO()) 64 | 65 | // Use the IndexExists service to check if a specified index exists. 66 | exists, err := client.IndexExists(indexFunc()).Do(ctx) 67 | if err != nil { 68 | // Handle error 69 | return nil, err 70 | } 71 | if !exists { 72 | createIndex, err := client.CreateIndex(indexFunc()).Do(ctx) 73 | if err != nil { 74 | return nil, err 75 | } 76 | if !createIndex.Acknowledged { 77 | return nil, ErrCannotCreateIndex 78 | } 79 | } 80 | 81 | return &ElasticHook{ 82 | client: client, 83 | host: host, 84 | index: indexFunc, 85 | levels: levels, 86 | ctx: ctx, 87 | ctxCancel: cancel, 88 | }, nil 89 | } 90 | 91 | // Fire is required to implement 92 | // Logrus hook 93 | func (hook *ElasticHook) Fire(entry *logrus.Entry) error { 94 | 95 | level := entry.Level.String() 96 | 97 | if e, ok := entry.Data[logrus.ErrorKey]; ok && e != nil { 98 | if err, ok := e.(error); ok { 99 | entry.Data[logrus.ErrorKey] = err.Error() 100 | } 101 | } 102 | 103 | msg := struct { 104 | Host string 105 | Timestamp string `json:"@timestamp"` 106 | Message string 107 | Data logrus.Fields 108 | Level string 109 | }{ 110 | hook.host, 111 | entry.Time.UTC().Format(time.RFC3339Nano), 112 | entry.Message, 113 | entry.Data, 114 | strings.ToUpper(level), 115 | } 116 | 117 | _, err := hook.client. 118 | Index(). 119 | Index(hook.index()). 120 | Type("log"). 121 | BodyJson(msg). 122 | Do(hook.ctx) 123 | 124 | return err 125 | } 126 | 127 | // Required for logrus 128 | // hook implementation 129 | func (hook *ElasticHook) Levels() []logrus.Level { 130 | return hook.levels 131 | } 132 | 133 | // Cancels all calls to 134 | // elastic 135 | func (hook *ElasticHook) Cancel() { 136 | hook.ctxCancel() 137 | } 138 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/brunoksato/golang-boilerplate/core" 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func AddPayloadWarning(c echo.Context, code int, message string) error { 12 | // payload["warning"] = map[string]interface{}{"code": code, "message": message} 13 | return nil 14 | } 15 | 16 | func AddPayloadError(c echo.Context, code int, message string) error { 17 | return c.JSON(http.StatusBadRequest, map[string]interface{}{"code": code, "message": message}) 18 | } 19 | 20 | func AddPermissionError(c echo.Context, code int, message string) error { 21 | return c.JSON(http.StatusForbidden, map[string]interface{}{"code": code, "message": message}) 22 | } 23 | 24 | func AddNotFoundError(c echo.Context, code int, message string) error { 25 | return c.JSON(http.StatusNotFound, map[string]interface{}{"code": code, "message": message}) 26 | } 27 | 28 | func AddServerError(c echo.Context, code int, message string) error { 29 | return c.JSON(http.StatusInternalServerError, map[string]interface{}{"code": code, "message": message}) 30 | } 31 | 32 | func AddDefaultError(c echo.Context, errModel core.DefaultError) error { 33 | var err error 34 | msg := fmt.Sprintf("%s (caller: %s)", errModel.Error(), errModel.Location()) 35 | 36 | code := errModel.Code() 37 | if errModel.Subcode() != 0 { 38 | code = errModel.Subcode() 39 | } 40 | 41 | params := errModel.Data() 42 | params["code"] = errModel.Code() 43 | params["subcode"] = errModel.Subcode() 44 | 45 | logger := LoggerForParams(c, params) 46 | 47 | switch errModel.Code() { 48 | case 300: 49 | logger.Warning("Warning: " + msg) 50 | err = AddPayloadWarning(c, code, errModel.Error()) 51 | case 400: 52 | logger.Info("Business Error: " + msg) 53 | err = AddPayloadError(c, code, errModel.Error()) 54 | case 403: 55 | logger.Warning("Permission Error: " + msg) 56 | err = AddPermissionError(c, code, errModel.Error()) 57 | case 404: 58 | logger.Info("Not Found: " + msg) 59 | err = AddNotFoundError(c, code, errModel.Error()) 60 | default: 61 | logger.Error("Server Error: " + msg) 62 | err = AddServerError(c, code, errModel.Error()) 63 | } 64 | 65 | return err 66 | } 67 | -------------------------------------------------------------------------------- /log/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "github.com/Sirupsen/logrus" 5 | "github.com/brunoksato/golang-boilerplate/model" 6 | "github.com/labstack/echo/v4" 7 | ) 8 | 9 | func Logger(c echo.Context) *logrus.Entry { 10 | return LoggerForParams(c, nil) 11 | } 12 | 13 | func LoggerForParams(c echo.Context, params map[string]interface{}) *logrus.Entry { 14 | fields := make(map[string]interface{}) 15 | if params != nil { 16 | for k, v := range params { 17 | fields[k] = v 18 | } 19 | } 20 | 21 | var user model.User 22 | if c.Get("User") != nil { 23 | user = c.Get("User").(model.User) 24 | fields["id-u"] = user.ID 25 | } else { 26 | fields["id-u"] = 0 27 | } 28 | 29 | fields["@application"] = c.Get("AppName").(string) 30 | fields["id-req"] = c.Get("RequestID").(string) 31 | fields["method"] = c.Get("Method").(string) 32 | fields["endpoint"] = c.Get("Endpoint").(string) 33 | fields["path"] = c.Get("Path").(string) 34 | 35 | if fields["system"] == nil { 36 | fields["system"] = "api" 37 | } 38 | 39 | return logrus.WithFields(logrus.Fields(fields)) 40 | } 41 | -------------------------------------------------------------------------------- /log/logstash.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/Sirupsen/logrus" 9 | ) 10 | 11 | const defaultTimestampFormat = time.RFC3339 12 | 13 | type LogstashFormatter struct { 14 | Type string // if not empty use for logstash type field. 15 | 16 | // TimestampFormat sets the format used for timestamps. 17 | TimestampFormat string 18 | } 19 | 20 | func (f *LogstashFormatter) Format(entry *logrus.Entry) ([]byte, error) { 21 | fields := make(logrus.Fields) 22 | for k, v := range entry.Data { 23 | fields[k] = v 24 | } 25 | 26 | fields["@version"] = 1 27 | 28 | timeStampFormat := f.TimestampFormat 29 | 30 | if timeStampFormat == "" { 31 | timeStampFormat = defaultTimestampFormat 32 | } 33 | 34 | fields["@timestamp"] = entry.Time.Format(timeStampFormat) 35 | 36 | // set message field 37 | v, ok := entry.Data["message"] 38 | if ok { 39 | fields["fields.message"] = v 40 | } 41 | fields["message"] = entry.Message 42 | 43 | // set level field 44 | v, ok = entry.Data["level"] 45 | if ok { 46 | fields["fields.level"] = v 47 | } 48 | fields["level"] = entry.Level.String() 49 | 50 | // set type field 51 | if f.Type != "" { 52 | v, ok = entry.Data["type"] 53 | if ok { 54 | fields["fields.type"] = v 55 | } 56 | fields["type"] = f.Type 57 | } 58 | 59 | serialized, err := json.Marshal(fields) 60 | if err != nil { 61 | return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) 62 | } 63 | return append(serialized, '\n'), nil 64 | } 65 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "time" 8 | 9 | "github.com/brunoksato/golang-boilerplate/server" 10 | _ "github.com/heroku/x/hmetrics/onload" 11 | ) 12 | 13 | func main() { 14 | server := server.Start() 15 | addr := ":" + os.Getenv("PORT") 16 | 17 | go func() { 18 | if err := server.Start(addr); err != nil { 19 | server.Logger.Info("shutting down the server") 20 | } 21 | }() 22 | 23 | quit := make(chan os.Signal) 24 | signal.Notify(quit, os.Interrupt) 25 | <-quit 26 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 27 | defer cancel() 28 | if err := server.Shutdown(ctx); err != nil { 29 | server.Logger.Fatal(err) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test ./model ./api ./util 3 | 4 | test-mid: 5 | go test ./middleware -v 6 | 7 | test-core: 8 | go test ./model -v 9 | 10 | test-util: 11 | go test ./util -v 12 | 13 | test-api: 14 | go test ./api -v 15 | 16 | deploy: 17 | git push heroku master -------------------------------------------------------------------------------- /middleware/configuration.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/brunoksato/golang-boilerplate/model" 5 | "github.com/jinzhu/gorm" 6 | "github.com/labstack/echo/v4" 7 | ) 8 | 9 | func LoadConfigurations(next echo.HandlerFunc) echo.HandlerFunc { 10 | return func(c echo.Context) error { 11 | db := c.Get("Database").(*gorm.DB) 12 | config := model.Configuration{} 13 | err := db.First(&config).Error 14 | if err == nil { 15 | c.Set("Configuration", config) 16 | } 17 | return next(c) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /middleware/db.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/brunoksato/golang-boilerplate/config" 5 | "github.com/jinzhu/gorm" 6 | "github.com/labstack/echo/v4" 7 | ) 8 | 9 | func DBMiddleware(db *gorm.DB) echo.MiddlewareFunc { 10 | return func(next echo.HandlerFunc) echo.HandlerFunc { 11 | return func(c echo.Context) error { 12 | if db == nil { 13 | db = config.InitDB() 14 | } 15 | 16 | c.Set("Database", db) 17 | 18 | return next(c) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /middleware/elastic.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/brunoksato/golang-boilerplate/config" 5 | "github.com/labstack/echo/v4" 6 | "github.com/olivere/elastic" 7 | ) 8 | 9 | func ElasticMiddleware(es *elastic.Client) echo.MiddlewareFunc { 10 | return func(next echo.HandlerFunc) echo.HandlerFunc { 11 | return func(c echo.Context) error { 12 | if es == nil { 13 | es = config.InitElasticSearchAndLogger() 14 | } 15 | 16 | c.Set("Elastic", es) 17 | 18 | return next(c) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /middleware/header.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/brunoksato/golang-boilerplate/core" 7 | "github.com/labstack/echo/v4" 8 | ) 9 | 10 | func SettingHeaders(next echo.HandlerFunc) echo.HandlerFunc { 11 | return func(c echo.Context) error { 12 | xCompany := c.Request().Header.Get("X-Company") 13 | switch xCompany { 14 | case "Office": 15 | c.Set("APIType", core.USER_API) 16 | case "CronJob": 17 | c.Set("APIType", core.CRONJOB_API) 18 | cCronjob := c.Request().Header.Get("X-Cronjob") 19 | if cCronjob != "youpassword" { 20 | return echo.NewHTTPError(http.StatusUnauthorized) 21 | } 22 | case "Admin": 23 | c.Set("APIType", core.ADMIN_API) 24 | default: 25 | return echo.NewHTTPError(http.StatusUnauthorized) 26 | } 27 | 28 | return next(c) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /middleware/initialize.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/labstack/echo/v4" 8 | uuid "github.com/satori/go.uuid" 9 | ) 10 | 11 | func InitializePayload(next echo.HandlerFunc) echo.HandlerFunc { 12 | return func(c echo.Context) error { 13 | newv4 := uuid.NewV4() 14 | c.Set("Payload", make(map[string]interface{})) 15 | c.Set("Request", make(map[string]interface{})) 16 | c.Set("AppName", fmt.Sprintf("%s-%s", os.Getenv("SERVER_NAME"), os.Getenv("SERVER_ENV"))) 17 | c.Set("RequestID", newv4.String()) 18 | c.Set("Method", c.Request().Method) 19 | c.Set("Endpoint", fmt.Sprintf("%s %s", c.Request().Method, c.Request().URL.Path)) 20 | c.Set("Path", c.Request().URL.String()) 21 | 22 | //TODO??? 23 | c.Response().After(func() { 24 | }) 25 | 26 | return next(c) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /middleware/session.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "strings" 7 | 8 | "github.com/brunoksato/golang-boilerplate/model" 9 | jwt "github.com/dgrijalva/jwt-go" 10 | "github.com/jinzhu/gorm" 11 | "github.com/labstack/echo/v4" 12 | ) 13 | 14 | func Session(next echo.HandlerFunc) echo.HandlerFunc { 15 | return func(c echo.Context) error { 16 | db := c.Get("Database").(*gorm.DB) 17 | user := model.User{} 18 | isPrivate := strings.Contains(c.Path(), "api") 19 | isAdmin := strings.Contains(c.Path(), "admin") 20 | if isPrivate || isAdmin { 21 | authorization := c.Request().Header.Get("Authorization") 22 | secretKey := os.Getenv("JWT_KEY_SIGNIN") 23 | if authorization != "" { 24 | tokenSlice := strings.Split(authorization, " ") 25 | if len(tokenSlice) == 2 && tokenSlice[0] == "Bearer" { 26 | token, err := model.VerifyJWTToken(tokenSlice[1], secretKey) 27 | if err != nil { 28 | return echo.NewHTTPError(http.StatusUnauthorized) 29 | } 30 | 31 | claims := token.Claims.(jwt.MapClaims) 32 | if token.Valid && claims["iss"] == model.JWT_ISS { 33 | uidParse := claims["user"].(float64) 34 | uid := uint(uidParse) 35 | err := db. 36 | First(&user, uid). 37 | Error 38 | if err != nil { 39 | return echo.NewHTTPError(http.StatusUnauthorized) 40 | } 41 | 42 | if isAdmin { 43 | if !user.IsAdmin() { 44 | return echo.NewHTTPError(http.StatusUnauthorized) 45 | } 46 | } 47 | } else { 48 | return echo.NewHTTPError(http.StatusUnauthorized) 49 | } 50 | } else { 51 | return echo.NewHTTPError(http.StatusUnauthorized) 52 | } 53 | } else { 54 | return echo.NewHTTPError(http.StatusUnauthorized) 55 | } 56 | } else { 57 | user.ID = 0 58 | } 59 | 60 | c.Set("User", user) 61 | 62 | return next(c) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /middleware/type.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | 7 | "github.com/brunoksato/golang-boilerplate/model" 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func DetermineType(next echo.HandlerFunc) echo.HandlerFunc { 12 | return func(c echo.Context) error { 13 | var tType reflect.Type 14 | var parentType reflect.Type 15 | 16 | parts := PathParts(c.Path()) 17 | var pathType string 18 | for i := 0; i < len(parts); i++ { 19 | pathType = parts[i] 20 | t := StringToType(pathType) 21 | if t != nil { 22 | if tType != nil { 23 | parentType = tType 24 | } 25 | tType = t 26 | } 27 | } 28 | 29 | c.Set("ParentType", parentType) 30 | c.Set("Type", tType) 31 | 32 | return next(c) 33 | } 34 | } 35 | 36 | func PathParts(path string) []string { 37 | return strings.Split(strings.Trim(path, " /"), "/") 38 | } 39 | 40 | func StringToType(typeName string) (t reflect.Type) { 41 | switch typeName { 42 | case "users": 43 | var m model.User 44 | t = reflect.TypeOf(m) 45 | case "configurations": 46 | var m model.Configuration 47 | t = reflect.TypeOf(m) 48 | } 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /model/configuration.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/brunoksato/golang-boilerplate/core" 5 | ) 6 | 7 | type Configuration struct { 8 | Model 9 | MinValueBuy float64 `json:"min_value_buy"` 10 | } 11 | 12 | func (c Configuration) ValidateForCreate() core.DefaultError { 13 | err := ValidateStruct(c) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | return nil 19 | } 20 | 21 | func (c Configuration) ValidateForUpdate() core.DefaultError { 22 | err := ValidateStruct(c) 23 | if err != nil { 24 | return err 25 | } 26 | return nil 27 | } 28 | 29 | func (c Configuration) ValidateForDelete(ctx *ModelCtx) core.DefaultError { 30 | return nil 31 | } 32 | 33 | func (c Configuration) ValidateField(f string) core.DefaultError { 34 | err := ValidateStructField(c, f) 35 | 36 | return err 37 | } 38 | 39 | // Restrictor 40 | 41 | func (c Configuration) UserCanView(ctx *ModelCtx, viewer User) (bool, core.DefaultError) { 42 | return viewer.IsAdmin(), nil 43 | } 44 | 45 | func (c Configuration) UserCanCreate(ctx *ModelCtx, creator User) (bool, core.DefaultError) { 46 | return creator.IsAdmin(), nil 47 | } 48 | 49 | func (c Configuration) UserCanUpdate(ctx *ModelCtx, updater User, fields []string) (bool, core.DefaultError) { 50 | return updater.IsAdmin(), nil 51 | } 52 | 53 | func (c Configuration) UserCanDelete(ctx *ModelCtx, deleter User) (bool, core.DefaultError) { 54 | return deleter.IsAdmin(), nil 55 | } 56 | 57 | // Business methods 58 | 59 | // Scopes 60 | -------------------------------------------------------------------------------- /model/context.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/Sirupsen/logrus" 5 | "github.com/brunoksato/golang-boilerplate/core" 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | type ModelCtx struct { 10 | RequestID string 11 | APIType core.APIType 12 | Database *gorm.DB 13 | User User 14 | Configuration Configuration 15 | Logger *logrus.Entry 16 | } 17 | -------------------------------------------------------------------------------- /model/creator.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/brunoksato/golang-boilerplate/core" 7 | ) 8 | 9 | type Creator interface { 10 | Create(*ModelCtx, User) core.DefaultError 11 | } 12 | 13 | func IsCreator(t reflect.Type) bool { 14 | modelType := reflect.TypeOf((*Creator)(nil)).Elem() 15 | return t.Implements(modelType) 16 | } 17 | -------------------------------------------------------------------------------- /model/creator_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/brunoksato/golang-boilerplate/core" 8 | ) 9 | 10 | func TestIsCreator(t *testing.T) { 11 | core.AssertTrue(t, IsCreator(reflect.TypeOf(&User{}))) 12 | core.AssertFalse(t, IsCreator(reflect.TypeOf(&Configuration{}))) 13 | } -------------------------------------------------------------------------------- /model/db_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/Sirupsen/logrus" 8 | "github.com/brunoksato/golang-boilerplate/core" 9 | _ "github.com/jinzhu/gorm/dialects/postgres" 10 | ) 11 | 12 | func init() { 13 | db := InitTestDB() 14 | INITDB = db 15 | } 16 | 17 | func setupDB() { 18 | TESTDB = INITDB.Begin() 19 | CTX = &ModelCtx{} 20 | CTX.Database = TESTDB 21 | CTX.Logger = logrus.WithFields(logrus.Fields{}) 22 | CTX.APIType = core.USER_API 23 | } 24 | 25 | func TestTableNameForUser(t *testing.T) { 26 | expected := "users" 27 | ty := reflect.TypeOf(User{}) 28 | actual := core.TableNameFor(ty) 29 | core.AssertEqual(t, expected, actual) 30 | } 31 | 32 | 33 | func TestTableNameForConfiguration(t *testing.T) { 34 | expected := "configurations" 35 | ty := reflect.TypeOf(Configuration{}) 36 | actual := core.TableNameFor(ty) 37 | core.AssertEqual(t, expected, actual) 38 | } -------------------------------------------------------------------------------- /model/deleter.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/brunoksato/golang-boilerplate/core" 8 | ) 9 | 10 | type Deleter interface { 11 | Delete(*ModelCtx, User) core.DefaultError 12 | ValidateForDelete(*ModelCtx) core.DefaultError 13 | } 14 | 15 | func IsDeleter(t reflect.Type) bool { 16 | modelType := reflect.TypeOf((*Deleter)(nil)).Elem() 17 | return t.Implements(modelType) 18 | } 19 | 20 | func DefaultDelete(ctx *ModelCtx, item interface{}) core.DefaultError { 21 | itemID, err := core.GetID(item) 22 | if err != nil || itemID == 0 { 23 | return core.NewNotFoundError(fmt.Sprintf("A %v must exist in the database to be deleted", reflect.TypeOf(item))) 24 | } 25 | 26 | db := ctx.Database 27 | err = db.Set("gorm:save_associations", false).Delete(item).Error 28 | if err != nil { 29 | return core.NewServerError("Database error while deleting: " + err.Error()) 30 | } 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /model/deleter_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/brunoksato/golang-boilerplate/core" 8 | ) 9 | 10 | func TestIsDeleter(t *testing.T) { 11 | core.AssertFalse(t, IsDeleter(reflect.TypeOf(&User{}))) 12 | core.AssertFalse(t, IsDeleter(reflect.TypeOf(&Configuration{}))) 13 | } 14 | -------------------------------------------------------------------------------- /model/getter.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/brunoksato/golang-boilerplate/core" 7 | ) 8 | 9 | type Getter interface { 10 | GetByID(*ModelCtx, User, uint) (Getter, core.DefaultError) 11 | } 12 | 13 | func IsGetter(t reflect.Type) bool { 14 | modelType := reflect.TypeOf((*Getter)(nil)).Elem() 15 | return t.Implements(modelType) 16 | } 17 | -------------------------------------------------------------------------------- /model/getter_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/brunoksato/golang-boilerplate/core" 8 | ) 9 | 10 | func TestIsGetter(t *testing.T) { 11 | core.AssertFalse(t, IsGetter(reflect.TypeOf(&User{}))) 12 | core.AssertFalse(t, IsGetter(reflect.TypeOf(&Configuration{}))) 13 | } 14 | -------------------------------------------------------------------------------- /model/jwt.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "time" 8 | 9 | jwt "github.com/dgrijalva/jwt-go" 10 | ) 11 | 12 | const JWT_ISS = "server" 13 | 14 | func IssueJWToken(uid uint, roles []string, exp time.Time) (string, error) { 15 | if len(roles) == 0 { 16 | roles = []string{"user"} 17 | } 18 | 19 | claims := jwt.MapClaims{} 20 | claims["iss"] = JWT_ISS 21 | claims["user"] = uid 22 | claims["roles"] = roles 23 | claims["exp"] = exp.Unix() 24 | 25 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 26 | 27 | secretKey := []byte(os.Getenv("JWT_KEY_SIGNIN")) 28 | tokenString, err := token.SignedString(secretKey) 29 | 30 | if err != nil { 31 | return tokenString, fmt.Errorf("Couldn't issue token: %v", err) 32 | } 33 | 34 | return tokenString, nil 35 | } 36 | 37 | func IssueJWTTokenForEmail(uid uint, email string, exp time.Time) (string, error) { 38 | token := jwt.New(jwt.SigningMethodHS256) 39 | 40 | claims := jwt.MapClaims{} 41 | claims["iss"] = JWT_ISS 42 | claims["user"] = uid 43 | claims["email"] = email 44 | claims["exp"] = exp.Unix() 45 | token.Claims = claims 46 | secretKey := []byte(os.Getenv("JWT_KEY_EMAIL")) 47 | tokenString, err := token.SignedString(secretKey) 48 | 49 | if err != nil { 50 | return tokenString, fmt.Errorf("Couldn't issue token: %v", err) 51 | } 52 | 53 | return tokenString, nil 54 | } 55 | 56 | func JWTTokenExpirationDate() time.Time { 57 | var err error 58 | var jwtHours int 59 | jwtHours, err = strconv.Atoi(os.Getenv("JWT_TOKEN_EXPIRATION")) 60 | if err != nil { 61 | jwtHours = 24 62 | } 63 | jwtDuration := time.Duration(jwtHours) * time.Hour 64 | return (time.Now().Add(jwtDuration)).Round(time.Millisecond) 65 | } 66 | 67 | func VerifyJWTToken(tokenString, secretKey string) (*jwt.Token, error) { 68 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 69 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 70 | return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) 71 | } 72 | return []byte(secretKey), nil 73 | }) 74 | 75 | // Print an error if we can't parse for some reason 76 | if err != nil { 77 | return token, fmt.Errorf("Couldn't parse token: %v", err) 78 | } 79 | 80 | // Is token invalid? 81 | if !token.Valid { 82 | return token, fmt.Errorf("Token is invalid") 83 | } 84 | 85 | return token, nil 86 | } 87 | -------------------------------------------------------------------------------- /model/jwt_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/brunoksato/golang-boilerplate/core" 9 | ) 10 | 11 | func TestJWTTokenExpirationDate(t *testing.T) { 12 | jwtTime := time.Now().Add(time.Hour * 24).Round(time.Second) 13 | core.AssertEqual(t, jwtTime, JWTTokenExpirationDate().Round(time.Second)) 14 | 15 | os.Setenv("JWT_TOKEN_EXPIRATION", "0") 16 | jwtTime = time.Now().Add(time.Hour * 0).Round(time.Second) 17 | core.AssertEqual(t, jwtTime, JWTTokenExpirationDate().Round(time.Second)) 18 | 19 | os.Setenv("JWT_TOKEN_EXPIRATION", "2") 20 | jwtTime = time.Now().Add(time.Hour * 2).Round(time.Second) 21 | core.AssertEqual(t, jwtTime, JWTTokenExpirationDate().Round(time.Second)) 22 | 23 | os.Setenv("JWT_TOKEN_EXPIRATION", "") // defaults to 24h if the conversion is wrong 24 | jwtTime = time.Now().Add(time.Hour * 24).Round(time.Second) 25 | core.AssertEqual(t, jwtTime, JWTTokenExpirationDate().Round(time.Second)) 26 | 27 | os.Setenv("JWT_TOKEN_EXPIRATION", "24") 28 | } 29 | -------------------------------------------------------------------------------- /model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/brunoksato/golang-boilerplate/core" 10 | "github.com/brunoksato/golang-boilerplate/util" 11 | "github.com/jinzhu/gorm" 12 | ) 13 | 14 | type Model struct { 15 | ID uint `json:"id" gorm:"primary_key" settable:"false"` 16 | CreatedAt time.Time `json:"created_at" sql:"DEFAULT:current_timestamp" settable:"false"` 17 | UpdatedAt time.Time `json:"updated_at" sql:"DEFAULT:current_timestamp" settable:"false"` 18 | } 19 | 20 | func (m Model) OrderBy(db *gorm.DB) *gorm.DB { 21 | order := "\"created_at\" DESC" 22 | return db.Order(order) 23 | } 24 | 25 | func ParentIdField(t reflect.Type) (field *reflect.StructField, dbFieldName string) { 26 | elemT := t 27 | if elemT.Kind() == reflect.Ptr { 28 | elemT = elemT.Elem() 29 | } 30 | for i := 0; i < elemT.NumField(); i++ { 31 | tag := elemT.Field(i).Tag 32 | if tag.Get("parent_field") != "" { 33 | dbFieldName = tag.Get("parent_field") 34 | fieldRef := elemT.Field(i) 35 | field = &fieldRef 36 | } 37 | } 38 | return 39 | } 40 | 41 | func UserIDField(t reflect.Type) (field *reflect.StructField, dbFieldName string) { 42 | elemT := t 43 | if elemT.Kind() == reflect.Ptr { 44 | elemT = elemT.Elem() 45 | } 46 | 47 | f, found := elemT.FieldByName("UserID") 48 | if found { 49 | field = &f 50 | dbFieldName = "user_id" 51 | } 52 | return 53 | } 54 | 55 | func OrderField(t reflect.Type) (field *reflect.StructField, dbFieldName string) { 56 | elemT := t 57 | if elemT.Kind() == reflect.Ptr { 58 | elemT = elemT.Elem() 59 | } 60 | for i := 0; i < elemT.NumField(); i++ { 61 | tag := elemT.Field(i).Tag 62 | if tag.Get("order_field") != "" { 63 | dbFieldName = tag.Get("order_field") 64 | fieldRef := elemT.Field(i) 65 | field = &fieldRef 66 | } 67 | } 68 | return 69 | } 70 | 71 | func SetUserID(item interface{}, id uint) error { 72 | v := reflect.ValueOf(item) 73 | if v.Kind() == reflect.Ptr { 74 | if v.IsNil() { 75 | return errors.New("Cannot set value on nil item") 76 | } 77 | v = reflect.ValueOf(item).Elem() 78 | } 79 | t := v.Type() 80 | if !TypeHasUserField(t) { 81 | return errors.New("Type does not have a reference to a user") 82 | } 83 | userField, _ := UserIDField(t) 84 | f := v.FieldByName(userField.Name) 85 | if f.Type().Kind() == reflect.Ptr { 86 | f.Set(reflect.ValueOf(&id)) 87 | } else { 88 | f.Set(reflect.ValueOf(id)) 89 | } 90 | return nil 91 | } 92 | 93 | func TypeHasParentField(t reflect.Type) bool { 94 | parentField, _ := ParentIdField(t) 95 | return parentField != nil 96 | } 97 | 98 | func TypeHasUserField(t reflect.Type) bool { 99 | userField, _ := UserIDField(t) 100 | return userField != nil 101 | } 102 | 103 | func AssertUserCan(t *testing.T, m func(ctx *ModelCtx, u User) (bool, core.DefaultError), ctx *ModelCtx, user User) { 104 | can, err := m(ctx, user) 105 | if err != nil { 106 | t.Errorf("assertUserCan: Error occurred - %s (caller: %s)", err.Error(), util.CallerInfo()) 107 | } 108 | if !can { 109 | t.Errorf("assertUserCan: User cannot (caller: %s)", util.CallerInfo()) 110 | } 111 | } 112 | 113 | func AssertUserCant(t *testing.T, m func(ctx *ModelCtx, u User) (bool, core.DefaultError), ctx *ModelCtx, user User) { 114 | can, err := m(ctx, user) 115 | if err != nil { 116 | t.Errorf("assertUserCant: Error occurred - %s (caller: %s)", err.Error(), util.CallerInfo()) 117 | } 118 | if can { 119 | t.Errorf("assertUserCant: User can (caller: %s)", util.CallerInfo()) 120 | } 121 | } 122 | 123 | func AssertUserCanUpdate(t *testing.T, m func(ctx *ModelCtx, u User, s []string) (bool, core.DefaultError), ctx *ModelCtx, user User, fields []string) { 124 | can, err := m(ctx, user, fields) 125 | if err != nil { 126 | t.Errorf("assertUserCanUpdate: Error occurred - %s (caller: %s)", err.Error(), util.CallerInfo()) 127 | } 128 | if !can { 129 | t.Errorf("assertUserCanUpdate: User cannot (caller: %s)", util.CallerInfo()) 130 | } 131 | } 132 | 133 | func AssertUserCantUpdate(t *testing.T, m func(ctx *ModelCtx, u User, s []string) (bool, core.DefaultError), ctx *ModelCtx, user User, fields []string) { 134 | can, err := m(ctx, user, fields) 135 | if err != nil { 136 | t.Errorf("assertUserCantUpdate: Error occurred - %s (caller: %s)", err.Error(), util.CallerInfo()) 137 | } 138 | if can { 139 | t.Errorf("assertUserCantUpdate: User can (caller: %s)", util.CallerInfo()) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /model/restrictor.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/brunoksato/golang-boilerplate/core" 7 | ) 8 | 9 | type Restrictor interface { 10 | UserCanView(ctx *ModelCtx, u User) (bool, core.DefaultError) 11 | UserCanCreate(ctx *ModelCtx, u User) (bool, core.DefaultError) 12 | UserCanUpdate(ctx *ModelCtx, u User, fields []string) (bool, core.DefaultError) 13 | UserCanDelete(ctx *ModelCtx, u User) (bool, core.DefaultError) 14 | } 15 | 16 | func IsRestrictor(t reflect.Type) bool { 17 | modelType := reflect.TypeOf((*Restrictor)(nil)).Elem() 18 | return t.Implements(modelType) 19 | } 20 | -------------------------------------------------------------------------------- /model/restrictor_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/brunoksato/golang-boilerplate/core" 8 | ) 9 | 10 | func TestIsRestrictor(t *testing.T) { 11 | core.AssertTrue(t, IsRestrictor(reflect.TypeOf(User{}))) 12 | core.AssertTrue(t, IsRestrictor(reflect.TypeOf(Configuration{}))) 13 | } 14 | -------------------------------------------------------------------------------- /model/sorter.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | type Sorter interface { 10 | OrderBy(*gorm.DB) *gorm.DB 11 | } 12 | 13 | func IsSorter(t reflect.Type) bool { 14 | modelType := reflect.TypeOf((*Sorter)(nil)).Elem() 15 | return t.Implements(modelType) 16 | } 17 | -------------------------------------------------------------------------------- /model/sorter_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/brunoksato/golang-boilerplate/core" 8 | "github.com/jinzhu/gorm" 9 | ) 10 | 11 | func TestSorterOverriding(t *testing.T) { 12 | setupDB() 13 | defer teardownDB() 14 | setupSorterDB() 15 | 16 | s1 := SorterTestStruct{Order: 1} 17 | s2 := SorterTestStruct{Order: 2} 18 | TESTDB.Create(&s1) 19 | TESTDB.Create(&s2) 20 | 21 | var sorters []SorterTestStruct 22 | db := s1.OrderBy(TESTDB) 23 | db.Find(&sorters) 24 | core.AssertEqual(t, 2, len(sorters)) 25 | core.AssertEqual(t, s1.ID, sorters[0].ID) 26 | core.AssertEqual(t, s2.ID, sorters[1].ID) 27 | } 28 | 29 | func TestIsSorter(t *testing.T) { 30 | core.AssertTrue(t, IsSorter(reflect.TypeOf(User{}))) 31 | core.AssertTrue(t, IsSorter(reflect.TypeOf(Configuration{}))) 32 | } 33 | 34 | type SorterTestStruct struct { 35 | Model 36 | Order uint 37 | } 38 | 39 | func (m SorterTestStruct) OrderBy(db *gorm.DB) *gorm.DB { 40 | order := "\"order\" asc" 41 | return db.Order(order) 42 | } 43 | 44 | func setupSorterDB() { 45 | TESTDB.AutoMigrate(&SorterTestStruct{}) 46 | } 47 | -------------------------------------------------------------------------------- /model/test_helper.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/brunoksato/golang-boilerplate/core" 8 | "github.com/jinzhu/gorm" 9 | "golang.org/x/crypto/bcrypt" 10 | ) 11 | 12 | var INITDB *gorm.DB 13 | var TESTDB *gorm.DB 14 | var CTX *ModelCtx 15 | 16 | func startTransaction() { 17 | TESTDB = INITDB.Begin() 18 | CTX.Database = TESTDB 19 | } 20 | 21 | func teardownDB() { 22 | TESTDB = TESTDB.Rollback() 23 | } 24 | 25 | func startDBLog() { 26 | TESTDB.LogMode(true) 27 | } 28 | 29 | func stopDBLog() { 30 | TESTDB.LogMode(false) 31 | } 32 | 33 | func InitTestDB() *gorm.DB { 34 | if os.Getenv("TEST_DB") == "" { 35 | os.Setenv("TEST_DB", "user=postgres dbname=server_test sslmode=disable") 36 | } else { 37 | os.Setenv("TEST_DB", os.Getenv("TEST_DB")) 38 | } 39 | 40 | var err error 41 | var db *gorm.DB 42 | if db, err = core.OpenTestConnection(); err != nil { 43 | fmt.Println("No error should happen when connecting to test database, but got", err) 44 | } 45 | 46 | if os.Getenv("TEST_DB_LOGMODE") == "true" { 47 | fmt.Println("Setting logmode to true") 48 | db.LogMode(true) 49 | } else { 50 | db.LogMode(false) 51 | } 52 | 53 | db.DB().SetMaxIdleConns(1) 54 | db.DB().SetMaxOpenConns(1) 55 | 56 | runMigration(db) 57 | 58 | return db 59 | } 60 | 61 | func runMigration(db *gorm.DB) { 62 | values := []interface{}{ 63 | &Configuration{}, 64 | &User{}, 65 | } 66 | 67 | for _, value := range values { 68 | derr := db.DropTableIfExists(value).Error 69 | if derr != nil { 70 | panic(fmt.Sprintf("Error dropping table %+v ", derr)) 71 | } 72 | } 73 | 74 | if err := db.AutoMigrate(values...).Error; err != nil { 75 | panic(fmt.Sprintf("No error should happen when create table, but got %+v", err)) 76 | } 77 | 78 | db.Exec("CREATE UNIQUE INDEX idx_users_email ON users USING btree (email);") 79 | db.Exec("CREATE UNIQUE INDEX idx_users_username ON users USING btree (username);") 80 | db.Exec("CREATE UNIQUE INDEX idx_lower_case_username ON users ((lower(username)));") 81 | 82 | SeedDatabase(db) 83 | } 84 | 85 | func DeleteAllCommitedEntities(db *gorm.DB) { 86 | err := db.Delete(&Configuration{}).Error 87 | if err != nil { 88 | fmt.Println("Error deleting Configuration", err) 89 | } 90 | err = db.Unscoped().Delete(&User{}).Error 91 | if err != nil { 92 | fmt.Println("Error deleting User", err) 93 | } 94 | } 95 | 96 | func SeedDatabase(db *gorm.DB) { 97 | config := Configuration{ 98 | Model: Model{ID: 1}, 99 | MinValueBuy: 25.0, 100 | } 101 | db.Create(&config) 102 | 103 | password := "123456" 104 | hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 105 | user := User{Model: Model{ID: 999}, Name: "System", Email: "system@model.com", Username: "system", HashedPassword: hashedPassword} 106 | db.Create(&user) 107 | } 108 | -------------------------------------------------------------------------------- /model/updater.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/brunoksato/golang-boilerplate/core" 7 | ) 8 | 9 | type Updater interface { 10 | Update(*ModelCtx, User) core.DefaultError 11 | } 12 | 13 | func IsUpdater(t reflect.Type) bool { 14 | modelType := reflect.TypeOf((*Updater)(nil)).Elem() 15 | return t.Implements(modelType) 16 | } 17 | -------------------------------------------------------------------------------- /model/updater_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/brunoksato/golang-boilerplate/core" 8 | ) 9 | 10 | func TestIsUpdater(t *testing.T) { 11 | core.AssertTrue(t, IsUpdater(reflect.TypeOf(&User{}))) 12 | core.AssertFalse(t, IsUpdater(reflect.TypeOf(&Configuration{}))) 13 | } 14 | 15 | -------------------------------------------------------------------------------- /model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "github.com/brunoksato/golang-boilerplate/core" 10 | "github.com/jinzhu/gorm" 11 | "github.com/sendgrid/sendgrid-go" 12 | "golang.org/x/crypto/bcrypt" 13 | ) 14 | 15 | type User struct { 16 | Model 17 | DeletedAt *core.NullableTimestamp `json:"deleted_at,omitempty" settable:"false"` 18 | LastLogin *core.NullableTimestamp `json:"last_login,omitempty"` 19 | Name string `json:"name" sql:"not null" valid:"length(3|255),required"` 20 | Username string `json:"username" sql:"not null" valid:"length(3|15),matches(^[a-zA-Z0-9][a-zA-Z0-9-_]+$),required"` 21 | Email string `json:"email" sql:"not null" valid:"email,required"` 22 | Password string `json:"password,omitempty" sql:"-" valid:"length(5|64)"` 23 | HashedPassword []byte `json:"-" sql:"hashed_password;not null" gorm:"size:32"` 24 | Image string `json:"image"` 25 | Phone string `json:"phone"` 26 | Balance float64 `json:"balance" sql:"default:0"` 27 | Admin bool `json:"admin"` 28 | Ban bool `json:"ban"` 29 | } 30 | 31 | func (u User) ValidateForCreate() core.DefaultError { 32 | err := u.ValidateField("name") 33 | if err != nil { 34 | return err 35 | } 36 | err = u.ValidateField("username") 37 | if err != nil { 38 | return err 39 | } 40 | err = u.ValidateField("email") 41 | if err != nil { 42 | return err 43 | } 44 | err = u.ValidateField("password") 45 | if err != nil { 46 | return err 47 | } 48 | err = u.ValidateField("phone") 49 | if err != nil { 50 | return err 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func (u User) ValidateForUpdate() core.DefaultError { 57 | err := u.ValidateField("name") 58 | if err != nil { 59 | return err 60 | } 61 | err = u.ValidateField("email") 62 | if err != nil { 63 | return err 64 | } 65 | err = u.ValidateField("phone") 66 | if err != nil { 67 | return err 68 | } 69 | return nil 70 | } 71 | 72 | func (u User) ValidateForDelete(ctx *ModelCtx) core.DefaultError { 73 | return nil 74 | } 75 | 76 | func (u User) ValidateField(f string) core.DefaultError { 77 | data := map[string]interface{}{"field": f} 78 | err := ValidateStructField(u, f) 79 | 80 | if err != nil { 81 | switch f { 82 | case "email": 83 | if strings.Contains(err.Error(), "validate as email") { 84 | err = core.NewBusinessError(err.Error(), core.ERROR_SUBCODE_EMAIL_FORMAT, data) 85 | } 86 | case "name": 87 | if strings.Contains(err.Error(), "validate as matches") { 88 | err = core.NewBusinessError(err.Error(), core.ERROR_SUBCODE_NAME_FORMAT, data) 89 | } else if strings.Contains(err.Error(), "validate as length") { 90 | err = core.NewBusinessError(err.Error(), core.ERROR_SUBCODE_NAME_LENGTH, data) 91 | } 92 | case "username": 93 | if strings.Contains(err.Error(), "validate as matches") { 94 | err = core.NewBusinessError(err.Error(), core.ERROR_SUBCODE_USERNAME_FORMAT, data) 95 | } else if strings.Contains(err.Error(), "validate as length") { 96 | err = core.NewBusinessError(err.Error(), core.ERROR_SUBCODE_USERNAME_LENGTH, data) 97 | } 98 | case "phone": 99 | if strings.Contains(err.Error(), "validate as matches") { 100 | err = core.NewBusinessError(err.Error(), core.ERROR_SUBCODE_PHONE_TAKEN, data) 101 | } else if strings.Contains(err.Error(), "validate as length") { 102 | err = core.NewBusinessError(err.Error(), core.ERROR_SUBCODE_PHONE_LENGTH, data) 103 | } 104 | case "password": 105 | if strings.Contains(err.Error(), "validate as password") { 106 | err = core.NewBusinessError(err.Error(), core.ERROR_SUBCODE_PASSWORD_FORMAT, data) 107 | } else if strings.Contains(err.Error(), "validate as length") { 108 | err = core.NewBusinessError(err.Error(), core.ERROR_SUBCODE_PASSWORD_LENGTH, data) 109 | } 110 | } 111 | } 112 | 113 | return err 114 | } 115 | 116 | // Restrictor 117 | 118 | func (u User) UserCanView(ctx *ModelCtx, viewer User) (bool, core.DefaultError) { 119 | return true, nil 120 | } 121 | 122 | func (u User) UserCanCreate(ctx *ModelCtx, creator User) (bool, core.DefaultError) { 123 | return true, nil 124 | } 125 | 126 | func (u User) UserCanUpdate(ctx *ModelCtx, updater User, fields []string) (bool, core.DefaultError) { 127 | if u.ID == updater.ID { 128 | return true, nil 129 | } 130 | return false, nil 131 | } 132 | 133 | func (u User) UserCanDelete(ctx *ModelCtx, deleter User) (bool, core.DefaultError) { 134 | return deleter.IsAdmin(), nil 135 | } 136 | 137 | // Creator Interface 138 | func (u *User) Create(ctx *ModelCtx, creator User) core.DefaultError { 139 | db := ctx.Database 140 | 141 | err := u.ValidateForCreate() 142 | if err != nil { 143 | return err 144 | } 145 | 146 | var existing User 147 | dberr := db.Scopes(ByUserEmail(u.Email)).First(&existing).Error 148 | if dberr == nil { 149 | data := map[string]interface{}{ 150 | "creator_id": creator.ID, 151 | "name": u.Name, 152 | "email": u.Email, 153 | } 154 | return core.NewBusinessError( 155 | "email: already used;", 156 | core.ERROR_SUBCODE_EMAIL_TAKEN, 157 | data, 158 | ) 159 | } 160 | 161 | dberr = db.Scopes(ByUserUsername(u.Username)).First(&existing).Error 162 | if dberr == nil { 163 | data := map[string]interface{}{ 164 | "creator_id": creator.ID, 165 | "name": u.Name, 166 | "email": u.Email, 167 | } 168 | return core.NewBusinessError( 169 | "username: already used;", 170 | core.ERROR_SUBCODE_USERNAME_TAKEN, 171 | data, 172 | ) 173 | } 174 | 175 | // User bcrypt to generate HashedPassword 176 | u.HashedPassword, dberr = bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) 177 | if dberr != nil { 178 | data := map[string]interface{}{ 179 | "creator_id": creator.ID, 180 | "name": u.Name, 181 | "email": u.Email, 182 | } 183 | return core.NewServerError(dberr.Error(), data) 184 | } 185 | 186 | u.Password = "" 187 | 188 | dberr = db.Set("gorm:save_associations", false).Create(&u).Error 189 | if dberr != nil { 190 | data := map[string]interface{}{ 191 | "creator_id": creator.ID, 192 | "name": u.Name, 193 | "email": u.Email, 194 | } 195 | return core.NewServerError(dberr.Error(), data) 196 | } 197 | 198 | return nil 199 | } 200 | 201 | func (u *User) Update(ctx *ModelCtx, creator User) core.DefaultError { 202 | db := ctx.Database 203 | 204 | if u.Email != "" { 205 | var existing User 206 | dberr := db.Scopes(ByUserEmail(u.Email)).First(&existing).Error 207 | if dberr == nil { 208 | data := map[string]interface{}{ 209 | "creator_id": creator.ID, 210 | "name": u.Name, 211 | "email": u.Email, 212 | } 213 | return core.NewBusinessError( 214 | "email: already used;", 215 | core.ERROR_SUBCODE_EMAIL_TAKEN, 216 | data, 217 | ) 218 | } 219 | } 220 | 221 | dberr := db.Set("gorm:save_associations", false).Save(&u).Error 222 | if dberr != nil { 223 | data := map[string]interface{}{ 224 | "creator_id": creator.ID, 225 | "name": u.Name, 226 | "email": u.Email, 227 | } 228 | return core.NewServerError(dberr.Error(), data) 229 | } 230 | 231 | return nil 232 | } 233 | 234 | // Business methods 235 | 236 | func (u User) IsUser() bool { 237 | return !u.Admin 238 | } 239 | 240 | func (u User) IsAdmin() bool { 241 | return u.Admin 242 | } 243 | 244 | func (u User) VerifyPassword(password string) (bool, error) { 245 | return bcrypt.CompareHashAndPassword(u.HashedPassword, []byte(password)) == nil, nil 246 | } 247 | 248 | // Scopes 249 | func ByUserEmail(email string) func(*gorm.DB) *gorm.DB { 250 | return func(db *gorm.DB) *gorm.DB { 251 | return db.Where("LOWER(users.email) = LOWER(?)", email) 252 | } 253 | } 254 | 255 | func ByUserUsername(username string) func(*gorm.DB) *gorm.DB { 256 | return func(db *gorm.DB) *gorm.DB { 257 | return db.Where("LOWER(users.username) = LOWER(?)", username) 258 | } 259 | } 260 | 261 | // email 262 | func (u User) ResetPasswordEmail(token string) { 263 | request := sendgrid.GetRequest(os.Getenv("SENDGRID_KEY"), "/v3/mail/send", "https://api.sendgrid.com") 264 | request.Method = "POST" 265 | request.Body = []byte(`{ 266 | "personalizations": [ 267 | { 268 | "to": [ 269 | { 270 | "email": "` + u.Email + `" 271 | } 272 | ], 273 | "subject": "Forgot your password, ` + u.Name + `?", 274 | "dynamic_template_data": { 275 | "email":"` + u.Email + `", 276 | "name":"` + u.Name + `", 277 | "token":"` + token + `", 278 | } 279 | } 280 | ], 281 | "from": { 282 | "email": "` + os.Getenv("SENDGRID_USER") + `", 283 | "name": "Server" 284 | }, 285 | "template_id" : "d-aedb512d0ca7460cadc0027d561d9c25" 286 | }`) 287 | 288 | response, err := sendgrid.API(request) 289 | if err != nil { 290 | log.Println(err) 291 | } else { 292 | fmt.Println(response.Body) 293 | fmt.Println(response.Headers) 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /model/user_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/brunoksato/golang-boilerplate/core" 8 | ) 9 | 10 | func TestUserValidateForCreate(t *testing.T) { 11 | u := User{} 12 | u.Name = "Bruno Sato" 13 | u.Username = "brunoksato" 14 | u.Email = "bruno@model.com" 15 | u.Password = "123456" 16 | err := u.ValidateForCreate() 17 | core.AssertNoError(t, err) 18 | 19 | u.Email = "32" 20 | err = u.ValidateForCreate() 21 | core.AssertBusinessError(t, "email: 32 does not validate as email;", err) 22 | core.AssertEqual(t, core.ERROR_SUBCODE_EMAIL_FORMAT, err.Subcode()) 23 | 24 | u.Email = "bruno@model.com" 25 | err = u.ValidateForCreate() 26 | core.AssertNoError(t, err) 27 | 28 | u.Name = "u2" 29 | err = u.ValidateForCreate() 30 | core.AssertBusinessError(t, "name: u2 does not validate as length(3|255);", err) 31 | core.AssertEqual(t, core.ERROR_SUBCODE_NAME_LENGTH, err.Subcode()) 32 | 33 | u.Name = "Proper_Username" 34 | u.Password = "N" 35 | err = u.ValidateForCreate() 36 | core.AssertEqual(t, "password: N does not validate as length(5|64);", err.Error()) 37 | core.AssertEqual(t, core.ERROR_SUBCODE_PASSWORD_LENGTH, err.Subcode()) 38 | 39 | u.Password = "A valid passWord" 40 | 41 | u.Username = "u1" 42 | err = u.ValidateForCreate() 43 | core.AssertBusinessError(t, "username: u1 does not validate as length(3|15);", err) 44 | core.AssertEqual(t, core.ERROR_SUBCODE_USERNAME_LENGTH, err.Subcode()) 45 | 46 | u.Username = "brunoksato" 47 | 48 | err = u.ValidateForCreate() 49 | core.AssertNil(t, err) 50 | } 51 | 52 | func TestUserValidateForUpdate(t *testing.T) { 53 | u := User{} 54 | u.Name = "Bruno Sato" 55 | u.Username = "brunoksato" 56 | u.Email = "bruno@model.com" 57 | u.Password = "123456" 58 | err := u.ValidateForUpdate() 59 | core.AssertNoError(t, err) 60 | } 61 | 62 | func TestUserValidateForDelete(t *testing.T) { 63 | u := User{} 64 | err := u.ValidateForDelete(CTX) 65 | core.AssertNoError(t, err) 66 | } 67 | 68 | func TestUserRestrictor(t *testing.T) { 69 | setupDB() 70 | defer teardownDB() 71 | 72 | owner := User{Model: Model{ID: 1}, Name: "User 1", Email: "user1@model.com"} 73 | notOwner := User{Model: Model{ID: 1}, Name: "User 1", Email: "user1@model.com"} 74 | view := User{Model: Model{ID: 2}, Name: "User 2", Email: "user2@model.com"} 75 | 76 | AssertUserCan(t, owner.UserCanView, CTX, view) 77 | AssertUserCan(t, owner.UserCanCreate, CTX, view) 78 | AssertUserCantUpdate(t, owner.UserCanUpdate, CTX, view, []string{}) 79 | AssertUserCant(t, owner.UserCanDelete, CTX, view) 80 | 81 | AssertUserCan(t, owner.UserCanView, CTX, owner) 82 | AssertUserCan(t, owner.UserCanCreate, CTX, owner) 83 | AssertUserCanUpdate(t, owner.UserCanUpdate, CTX, owner, []string{}) 84 | AssertUserCant(t, owner.UserCanDelete, CTX, owner) 85 | 86 | AssertUserCan(t, notOwner.UserCanView, CTX, view) 87 | AssertUserCan(t, notOwner.UserCanCreate, CTX, view) 88 | AssertUserCantUpdate(t, notOwner.UserCanUpdate, CTX, view, []string{}) 89 | AssertUserCant(t, notOwner.UserCanDelete, CTX, view) 90 | } 91 | 92 | func TestByUserEmail(t *testing.T) { 93 | setupDB() 94 | defer teardownDB() 95 | 96 | u1 := User{Name: "User1", Email: "user1@model.com", Username: "user1"} 97 | u2 := User{Name: "User2", Email: "user2@model.com", Username: "user2"} 98 | u3 := User{Name: "User3", Email: "user3@model.com", Username: "user3"} 99 | u4 := User{Name: "User4", Email: "user4@model.com", Username: "user4"} 100 | u5 := User{Name: "User5"} 101 | TESTDB.Create(&u1) 102 | TESTDB.Create(&u2) 103 | TESTDB.Create(&u3) 104 | TESTDB.Create(&u4) 105 | TESTDB.Create(&u5) 106 | 107 | u := User{} 108 | err := TESTDB.Scopes(ByUserEmail(u1.Email)).First(&u).Error 109 | core.AssertNoError(t, err) 110 | core.AssertEqual(t, u1.ID, u.ID) 111 | 112 | u = User{} 113 | err = TESTDB.Scopes(ByUserEmail(strings.ToUpper(u2.Email))).First(&u).Error 114 | core.AssertNoError(t, err) 115 | core.AssertEqual(t, u2.ID, u.ID) 116 | 117 | u = User{} 118 | err = TESTDB.Scopes(ByUserEmail(strings.ToUpper(u3.Email))).First(&u).Error 119 | core.AssertNoError(t, err) 120 | core.AssertEqual(t, u3.ID, u.ID) 121 | 122 | u = User{} 123 | err = TESTDB.Scopes(ByUserEmail(strings.ToLower(u4.Email))).First(&u).Error 124 | core.AssertNoError(t, err) 125 | core.AssertEqual(t, u4.ID, u.ID) 126 | 127 | u = User{} 128 | err = TESTDB.Scopes(ByUserEmail("USeR_5")).First(&u).Error 129 | core.AssertEqual(t, "record not found", err.Error()) 130 | } 131 | 132 | func TestByUserUsername(t *testing.T) { 133 | setupDB() 134 | defer teardownDB() 135 | 136 | u1 := User{Name: "User1", Email: "user1@model.com", Username: "user1"} 137 | u2 := User{Name: "User2", Email: "user2@model.com", Username: "user2"} 138 | u3 := User{Name: "User3", Email: "user3@model.com", Username: "user3"} 139 | u4 := User{Name: "User4", Email: "user4@model.com", Username: "user4"} 140 | u5 := User{Name: "User5"} 141 | TESTDB.Create(&u1) 142 | TESTDB.Create(&u2) 143 | TESTDB.Create(&u3) 144 | TESTDB.Create(&u4) 145 | TESTDB.Create(&u5) 146 | 147 | u := User{} 148 | err := TESTDB.Scopes(ByUserUsername(u1.Username)).First(&u).Error 149 | core.AssertNoError(t, err) 150 | core.AssertEqual(t, u1.ID, u.ID) 151 | } 152 | 153 | func TestCreateUser(t *testing.T) { 154 | setupDB() 155 | defer teardownDB() 156 | 157 | us := []User{} 158 | TESTDB.Find(&us) 159 | core.AssertEqual(t, 0, len(us)) 160 | 161 | u := User{} 162 | u.Name = "Bruno Sato" 163 | u.Username = "brunoksato" 164 | u.Email = "bruno@model.com" 165 | u.Password = "123456" 166 | err := u.Create(CTX, User{}) 167 | core.AssertNoError(t, err) 168 | 169 | us = []User{} 170 | TESTDB.Find(&us) 171 | core.AssertEqual(t, 1, len(us)) 172 | 173 | u = User{Name: "User1", Email: "bruno@model.com", Username: "user1"} 174 | err = u.Create(CTX, User{}) 175 | core.AssertBusinessError(t, "email: already used;", err) 176 | core.AssertEqual(t, core.ERROR_SUBCODE_EMAIL_TAKEN, err.Subcode()) 177 | 178 | u = User{Name: "User1", Email: "bruno1@model.com", Username: "brunoksato"} 179 | err = u.Create(CTX, User{}) 180 | core.AssertBusinessError(t, "username: already used;", err) 181 | core.AssertEqual(t, core.ERROR_SUBCODE_EMAIL_TAKEN, err.Subcode()) 182 | } 183 | 184 | func TestUserIsAdmin(t *testing.T) { 185 | setupDB() 186 | defer teardownDB() 187 | 188 | u := User{} 189 | u.Email = "bruno@model.com" 190 | u.Name = "bruno" 191 | u.Password = "password" 192 | TESTDB.Create(&u) 193 | core.AssertFalse(t, u.IsAdmin()) 194 | 195 | u.Admin = true 196 | TESTDB.Save(&u) 197 | core.AssertTrue(t, u.IsAdmin()) 198 | } 199 | 200 | func TestUserIsUser(t *testing.T) { 201 | setupDB() 202 | defer teardownDB() 203 | 204 | u := User{} 205 | u.Email = "bruno@model.com" 206 | u.Name = "bruno" 207 | u.Password = "password" 208 | u.Admin = true 209 | TESTDB.Create(&u) 210 | core.AssertFalse(t, u.IsUser()) 211 | 212 | u.Admin = false 213 | TESTDB.Save(&u) 214 | core.AssertTrue(t, u.IsUser()) 215 | } 216 | -------------------------------------------------------------------------------- /model/validator.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/asaskevich/govalidator" 8 | "github.com/brunoksato/golang-boilerplate/core" 9 | "github.com/brunoksato/golang-boilerplate/util" 10 | ) 11 | 12 | type Validator interface { 13 | ValidateForCreate() core.DefaultError 14 | ValidateForUpdate() core.DefaultError 15 | ValidateForDelete(*ModelCtx) core.DefaultError 16 | ValidateField(string) core.DefaultError 17 | } 18 | 19 | func IsValidator(t reflect.Type) bool { 20 | modelType := reflect.TypeOf((*Validator)(nil)).Elem() 21 | return t.Implements(modelType) 22 | } 23 | 24 | func ValidateStruct(item interface{}) core.DefaultError { 25 | _, err := govalidator.ValidateStruct(item) 26 | if err != nil { 27 | return core.NewBusinessError(err.Error()) 28 | } 29 | return nil 30 | } 31 | 32 | func ValidateStructField(item interface{}, f string) core.DefaultError { 33 | _, err := govalidator.ValidateStruct(item) 34 | errStr := govalidator.ErrorByField(err, f) 35 | 36 | if errStr != "" { 37 | return core.NewBusinessError(fmt.Sprintf("%s: %s;", f, errStr)) 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func ValidateStructFields(item interface{}, fs []string) core.DefaultError { 44 | _, err := govalidator.ValidateStruct(item) 45 | if err == nil { 46 | return nil 47 | } 48 | 49 | result := "" 50 | for _, f := range fs { 51 | errStr := govalidator.ErrorByField(err, f) 52 | if errStr != "" { 53 | result = fmt.Sprintf("%s%s: %s;", result, f, errStr) 54 | } 55 | } 56 | 57 | if result == "" { 58 | return nil 59 | } 60 | 61 | return core.NewBusinessError(result) 62 | } 63 | 64 | func ValidateRequiredFields(item interface{}, fields []string) core.DefaultError { 65 | itemVal := reflect.ValueOf(item) 66 | errStr := "" 67 | 68 | for _, fName := range fields { 69 | f := itemVal.FieldByName(fName) 70 | if util.IsEmptyValue(f) { 71 | errStr = fmt.Sprintf("%s%s: non zero value required;", errStr, fName) 72 | } 73 | } 74 | 75 | if errStr != "" { 76 | return core.NewBusinessError(errStr) 77 | } 78 | 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /model/validator_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/brunoksato/golang-boilerplate/core" 8 | ) 9 | 10 | func TestIsValidator(t *testing.T) { 11 | core.AssertTrue(t, IsValidator(reflect.TypeOf(&User{}))) 12 | core.AssertTrue(t, IsValidator(reflect.TypeOf(&Configuration{}))) 13 | } 14 | 15 | func TestValidateStructNoErrors(t *testing.T) { 16 | v := ValidationTestStruct{ 17 | Required: "I'm here", 18 | NoValidation: "Who cares?", 19 | Numeric: "343452345", 20 | Float: "343452345.10", 21 | Email: "valid@model.com.br", 22 | } 23 | 24 | core.AssertNil(t, ValidateStruct(v)) 25 | } 26 | 27 | func TestValidateStructOneError(t *testing.T) { 28 | v := ValidationTestStruct{ 29 | Required: "I'm here", 30 | NoValidation: "Who cares?", 31 | Numeric: "343452345.10", 32 | Float: "343452345.10", 33 | Email: "valid@petmondo.com", 34 | } 35 | 36 | err := ValidateStruct(v) 37 | core.AssertEqual(t, "Numeric: 343452345.10 does not validate as numeric;", err.Error()) 38 | } 39 | 40 | func TestValidateStructMultipleErrors(t *testing.T) { 41 | v := ValidationTestStruct{ 42 | Required: "", 43 | Numeric: "343452345.10", 44 | Float: "343452345", 45 | Email: "invalid", 46 | DoubleValidation: "34t34*?$ˆ", 47 | } 48 | 49 | err := ValidateStruct(v) 50 | core.AssertEqual(t, "Required: non zero value required;Numeric: 343452345.10 does not validate as numeric;Email: invalid does not validate as email;DoubleValidation: 34t34*?$ˆ does not validate as alphanum;", err.Error()) 51 | } 52 | 53 | func TestValidateStructZeroStruct(t *testing.T) { 54 | v := ValidationTestStruct{} 55 | 56 | err := ValidateStruct(v) 57 | core.AssertEqual(t, "Required: non zero value required;", err.Error()) 58 | } 59 | 60 | func TestValidateStructField(t *testing.T) { 61 | v := ValidationTestStruct{ 62 | Required: "", 63 | Numeric: "343452345.10", 64 | Float: "343452345.10", 65 | Email: "bruno@model.com.br", 66 | } 67 | 68 | core.AssertNil(t, ValidateStructField(v, "NoValidation")) 69 | core.AssertNil(t, ValidateStructField(v, "Float")) 70 | core.AssertNil(t, ValidateStructField(v, "Email")) 71 | core.AssertEqual(t, "Required: non zero value required;", ValidateStructField(v, "Required").Error()) 72 | core.AssertEqual(t, "Numeric: 343452345.10 does not validate as numeric;", ValidateStructField(v, "Numeric").Error()) 73 | } 74 | 75 | func TestValidateStructDoubleValidationError(t *testing.T) { 76 | v := ValidationTestStruct{ 77 | DoubleValidation: "34t34**???+!@*&#$ˆ(((dfasdlfjasd;fl2903", 78 | } 79 | 80 | // Commenting this out, since the results are unpredictable - fails on Jekinns 81 | //AssertEqual(t, "DoubleValidation: 34t34**???+!@*&#$ˆ(((dfasdlfjasd;fl2903 does not validate as alphanum;", ValidateStructField(v, "DoubleValidation").Error()) 82 | 83 | v.DoubleValidation = "34t34**???" 84 | core.AssertEqual(t, "DoubleValidation: 34t34**??? does not validate as alphanum;", ValidateStructField(v, "DoubleValidation").Error()) 85 | 86 | v.DoubleValidation = "f8asd09f8asd0f98asf0as98df" 87 | core.AssertEqual(t, "DoubleValidation: f8asd09f8asd0f98asf0as98df does not validate as length(3|10);", ValidateStructField(v, "DoubleValidation").Error()) 88 | 89 | v.DoubleValidation = "f8asd09" 90 | core.AssertNil(t, ValidateStructField(v, "DoubleValidation")) 91 | } 92 | 93 | func TestValidateStructFields(t *testing.T) { 94 | v := ValidationTestStruct{ 95 | Required: "", 96 | Numeric: "343452345.10", 97 | Float: "343452345.10", 98 | Email: "bruno@model.com.br", 99 | DoubleValidation: "@(#*$@", 100 | } 101 | 102 | err := ValidateStructFields(v, []string{"Float", "Email"}) 103 | core.AssertNil(t, err) 104 | 105 | err = ValidateStructFields(v, []string{"Required", "Float", "Email"}) 106 | core.AssertEqual(t, "Required: non zero value required;", err.Error()) 107 | 108 | err = ValidateStructFields(v, []string{"Required", "Numeric", "Float", "Email"}) 109 | core.AssertEqual(t, "Required: non zero value required;Numeric: 343452345.10 does not validate as numeric;", err.Error()) 110 | } 111 | 112 | func TestValidateRequiredFields(t *testing.T) { 113 | vts := ValidationTestStruct{ 114 | Required: "And here", 115 | NoValidation: "", 116 | Numeric: "Here but wrong", 117 | Float: "", 118 | Email: "", 119 | DoubleValidation: "Make it triple", 120 | } 121 | core.AssertNil(t, ValidateRequiredFields(vts, []string{})) 122 | core.AssertNil(t, ValidateRequiredFields(vts, []string{"Required"})) 123 | core.AssertNil(t, ValidateRequiredFields(vts, []string{"Required", "Numeric", "DoubleValidation"})) 124 | core.AssertEqual(t, "NoValidation: non zero value required;", ValidateRequiredFields(vts, []string{"NoValidation"}).Error()) 125 | core.AssertEqual(t, "NoValidation: non zero value required;Float: non zero value required;Email: non zero value required;", ValidateRequiredFields(vts, []string{"Required", "NoValidation", "Numeric", "Float", "Email", "DoubleValidation"}).Error()) 126 | } 127 | 128 | type ValidationTestStruct struct { 129 | Required string `valid:"required"` 130 | NoValidation string `valid:"-"` 131 | Numeric string `valid:"numeric"` 132 | Float string `valid:"float"` 133 | Email string `valid:"email"` 134 | DoubleValidation string `valid:"alphanum,length(3|10)"` 135 | } 136 | -------------------------------------------------------------------------------- /server/router.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | 7 | "github.com/brunoksato/golang-boilerplate/api" 8 | middle "github.com/brunoksato/golang-boilerplate/middleware" 9 | "github.com/labstack/echo/v4" 10 | "github.com/labstack/echo/v4/middleware" 11 | "go.elastic.co/apm/module/apmechov4" 12 | ) 13 | 14 | var cors []string 15 | 16 | type MiddlewareConfigurer interface { 17 | ConfigureDefaultApiMiddleware(*echo.Echo) *echo.Echo 18 | ConfigurePublicApiMiddleware(*echo.Echo) *echo.Group 19 | ConfigurePrivateApiMiddleware(*echo.Echo) *echo.Group 20 | ConfigureCronJobApiMiddleware(*echo.Echo) *echo.Group 21 | ConfigureAdminApiMiddleware(*echo.Echo) *echo.Group 22 | } 23 | 24 | func SetupRouter(mc MiddlewareConfigurer) *echo.Echo { 25 | root := echo.New() 26 | root.Use(apmechov4.Middleware()) 27 | root.GET("/", func(c echo.Context) error { 28 | hello := map[string]interface{}{"status": "API OK"} 29 | return c.JSON(http.StatusOK, hello) 30 | }) 31 | 32 | root.POST("/webhook/sample", api.WebhookSample) 33 | 34 | if os.Getenv("ENV") == "production" { 35 | cors = []string{"*"} 36 | } else { 37 | cors = []string{"*"} 38 | } 39 | 40 | // 41 | // PUBLIC ENDPOINTS 42 | // 43 | public := mc.ConfigurePublicApiMiddleware(root) 44 | 45 | public.POST("/signin", api.SignIn) 46 | public.POST("/signup", api.SignUp) 47 | public.GET("/recover/:email", api.RecoverPassword) 48 | public.PUT("/change_password", api.ChangePasswordExternal) 49 | 50 | // 51 | // CRONJOB ENDPOINTS 52 | // 53 | cronjob := mc.ConfigureCronJobApiMiddleware(root) 54 | cronjob.GET("/sample", api.CronJobSample) 55 | // 56 | // PRIVATE ENDPOINTS 57 | // 58 | private := mc.ConfigurePrivateApiMiddleware(root) 59 | 60 | /* General */ 61 | private.GET("/logout", api.Logout) 62 | 63 | /* User */ 64 | private.GET("/users/me", api.Me) 65 | 66 | private.PUT("/users", api.UpdateUser) 67 | private.PUT("/users/password", api.ChangePassword) 68 | 69 | // 70 | // ADMIN ENDPOINTS 71 | // 72 | admin := mc.ConfigureAdminApiMiddleware(root) 73 | 74 | admin.GET("/users", api.List) 75 | 76 | return root 77 | } 78 | 79 | type ProductionMiddlewareConfigurer struct{} 80 | 81 | func (mc ProductionMiddlewareConfigurer) ConfigureDefaultApiMiddleware(root *echo.Echo) *echo.Echo { 82 | root.Use(middleware.Logger()) 83 | root.Use(middleware.Recover()) 84 | root.Use(middleware.CORS()) 85 | root.Use(middleware.SecureWithConfig(middleware.SecureConfig{ 86 | XSSProtection: "1; mode=block", 87 | ContentTypeNosniff: "nosniff", 88 | XFrameOptions: "SAMEORIGIN", 89 | ContentSecurityPolicy: "default-src 'self'", 90 | })) 91 | root.Use(middle.DBMiddleware(RW_DB_POOL)) 92 | root.Use(middle.ElasticMiddleware(ES)) 93 | root.Use(middle.DetermineType) 94 | root.Use(middle.InitializePayload) 95 | root.Use(middle.LoadConfigurations) 96 | 97 | return root 98 | } 99 | 100 | func (mc ProductionMiddlewareConfigurer) ConfigurePublicApiMiddleware(root *echo.Echo) *echo.Group { 101 | api := mc.ConfigureDefaultApiMiddleware(root) 102 | public := api.Group("/public") 103 | public.Use(middleware.CORS()) 104 | public.Use(middle.SettingHeaders) 105 | public.Use(middle.Session) 106 | 107 | return public 108 | } 109 | 110 | func (mc ProductionMiddlewareConfigurer) ConfigurePrivateApiMiddleware(root *echo.Echo) *echo.Group { 111 | api := mc.ConfigureDefaultApiMiddleware(root) 112 | private := api.Group("/api") 113 | private.Use(middleware.Gzip()) 114 | private.Use(middleware.CORS()) 115 | private.Use(middle.SettingHeaders) 116 | private.Use(middle.Session) 117 | 118 | return private 119 | } 120 | 121 | func (mc ProductionMiddlewareConfigurer) ConfigureCronJobApiMiddleware(root *echo.Echo) *echo.Group { 122 | api := mc.ConfigureDefaultApiMiddleware(root) 123 | private := api.Group("/cronjob") 124 | private.Use(middleware.CORS()) 125 | private.Use(middleware.Gzip()) 126 | private.Use(middle.SettingHeaders) 127 | private.Use(middle.Session) 128 | 129 | return private 130 | } 131 | 132 | func (mc ProductionMiddlewareConfigurer) ConfigureAdminApiMiddleware(root *echo.Echo) *echo.Group { 133 | api := mc.ConfigureDefaultApiMiddleware(root) 134 | private := api.Group("/admin") 135 | private.Use(middleware.CORS()) 136 | private.Use(middleware.Gzip()) 137 | private.Use(middle.SettingHeaders) 138 | private.Use(middle.Session) 139 | 140 | return private 141 | } 142 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "time" 5 | 6 | config "github.com/brunoksato/golang-boilerplate/config" 7 | "github.com/jinzhu/gorm" 8 | "github.com/labstack/echo/v4" 9 | "github.com/olivere/elastic" 10 | ) 11 | 12 | var RW_DB_POOL *gorm.DB 13 | var ES *elastic.Client 14 | 15 | func Start() *echo.Echo { 16 | config.Init() 17 | RW_DB_POOL = config.InitDB() 18 | RW_DB_POOL.DB().SetConnMaxLifetime(time.Second * 30) 19 | RW_DB_POOL.DB().SetMaxIdleConns(40) 20 | RW_DB_POOL.DB().SetMaxOpenConns(40) 21 | RW_DB_POOL.LogMode(true) 22 | ES = config.InitElasticSearchAndLogger() 23 | root := SetupRouter(ProductionMiddlewareConfigurer{}) 24 | 25 | return root 26 | } 27 | -------------------------------------------------------------------------------- /util/helper.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "reflect" 7 | "runtime" 8 | "strconv" 9 | "strings" 10 | "time" 11 | "unicode" 12 | ) 13 | 14 | func NewSliceForType(t reflect.Type) interface{} { 15 | items := reflect.New(reflect.SliceOf(t)).Interface() 16 | return items 17 | } 18 | 19 | func StringPtr(s string) *string { 20 | return &s 21 | } 22 | 23 | func UintPtr(u uint) *uint { 24 | return &u 25 | } 26 | 27 | func IntPtr(i int) *int { 28 | return &i 29 | } 30 | 31 | func Float32Ptr(f float32) *float32 { 32 | return &f 33 | } 34 | 35 | func Float64Ptr(f float64) *float64 { 36 | return &f 37 | } 38 | 39 | func TimePtr(t time.Time) *time.Time { 40 | return &t 41 | } 42 | 43 | func BoolPtr(b bool) *bool { 44 | return &b 45 | } 46 | 47 | func AUintToAInt(au []uint) []int { 48 | ai := make([]int, len(au)) 49 | for i := 0; i < len(au); i++ { 50 | ai[i] = int(au[i]) 51 | } 52 | return ai 53 | } 54 | 55 | func Atou(a string) (uint, error) { 56 | i, err := strconv.Atoi(a) 57 | if err != nil { 58 | return 0, err 59 | } 60 | return uint(i), nil 61 | } 62 | 63 | func Utoa(u uint) string { 64 | return fmt.Sprintf("%d", u) 65 | } 66 | 67 | func Capitalize(s string) string { 68 | if s == "" { 69 | return "" 70 | } 71 | if len(s) == 1 { 72 | return strings.ToUpper(s) 73 | } 74 | return strings.ToUpper(s[0:1]) + s[1:] 75 | } 76 | 77 | func CamelToSnake(s string) string { 78 | if s == "" { 79 | return "" 80 | } 81 | var result string 82 | var words []string 83 | var lastPos int 84 | rs := []rune(s) 85 | 86 | for i := 0; i < len(rs); i++ { 87 | if i > 0 && unicode.IsUpper(rs[i]) { 88 | words = append(words, s[lastPos:i]) 89 | lastPos = i 90 | } 91 | } 92 | 93 | // append the last word 94 | if s[lastPos:] != "" { 95 | words = append(words, s[lastPos:]) 96 | } 97 | 98 | for k, word := range words { 99 | if k > 0 { 100 | result += "_" 101 | } 102 | 103 | result += strings.ToLower(word) 104 | } 105 | 106 | return result 107 | } 108 | 109 | // SnakeToCamel returns a string converted from snake case to uppercase 110 | func SnakeToCamel(s string) string { 111 | if s == "" { 112 | return "" 113 | } 114 | var result string 115 | 116 | words := strings.Split(s, "_") 117 | 118 | for _, word := range words { 119 | w := []rune(word) 120 | w[0] = unicode.ToUpper(w[0]) 121 | result += string(w) 122 | } 123 | 124 | return result 125 | } 126 | 127 | func NanoToMicro(n int64) int64 { 128 | // Conversion to float64 was leading to imperfect results! 129 | //return int64(Round(float64(n) / 1000.0)) 130 | return (n + 500) / 1000 131 | } 132 | 133 | func MicroToNano(ms int64) int64 { 134 | return ms * 1000 135 | } 136 | 137 | func DurationInMs(start, end time.Time) int64 { 138 | return (end.UnixNano() - start.UnixNano()) / 1000000 139 | } 140 | 141 | func IsZeroStruct(item interface{}) bool { 142 | zero := false 143 | 144 | if item != nil { 145 | v := reflect.ValueOf(item) 146 | t := v.Type() 147 | k := t.Kind() 148 | 149 | if k == reflect.Struct { 150 | z := reflect.Zero(t) 151 | if reflect.DeepEqual(v.Interface(), z.Interface()) { 152 | zero = true 153 | } 154 | } 155 | } 156 | 157 | return zero 158 | } 159 | 160 | func Round(num float64) int { 161 | return int(num + math.Copysign(0.5, num)) 162 | } 163 | 164 | func RoundToDecimal(f float64, decimals int) float64 { 165 | factor := math.Pow(10, float64(decimals)) 166 | res := f * factor 167 | res = RoundFloat(res) 168 | res = res / factor 169 | return res 170 | } 171 | 172 | func RoundFloat(f float64) float64 { 173 | return math.Floor(f + .5) 174 | } 175 | 176 | func RoundPlus(f float64, places int) float64 { 177 | shift := math.Pow(10, float64(places)) 178 | return RoundFloat(f*shift) / shift 179 | } 180 | 181 | func ToFixed(num float64, precision int) float64 { 182 | output := math.Pow(10, float64(precision)) 183 | return float64(Round(num*output)) / output 184 | } 185 | 186 | func IsEmptyValue(v reflect.Value) bool { 187 | switch v.Kind() { 188 | case reflect.String, reflect.Array: 189 | return v.Len() == 0 190 | case reflect.Map, reflect.Slice: 191 | return v.Len() == 0 || v.IsNil() 192 | case reflect.Bool: 193 | return !v.Bool() 194 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 195 | return v.Int() == 0 196 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 197 | return v.Uint() == 0 198 | case reflect.Float32, reflect.Float64: 199 | return v.Float() == 0 200 | case reflect.Interface, reflect.Ptr: 201 | return v.IsNil() 202 | } 203 | 204 | return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface()) 205 | } 206 | 207 | func CallerInfo() string { 208 | _, file, line, ok := runtime.Caller(2) 209 | if !ok { 210 | return "" 211 | } 212 | parts := strings.Split(file, "/") 213 | file = parts[len(parts)-1] 214 | return fmt.Sprintf("%s:%d", file, line) 215 | } 216 | 217 | func ParentCallerInfo() string { 218 | _, file, line, ok := runtime.Caller(3) 219 | if !ok { 220 | return "" 221 | } 222 | parts := strings.Split(file, "/") 223 | file = parts[len(parts)-1] 224 | return fmt.Sprintf("%s:%d", file, line) 225 | } 226 | 227 | func ItemsOrEmptySlice(t reflect.Type, items interface{}) interface{} { 228 | if reflect.ValueOf(items).IsNil() { 229 | items = reflect.MakeSlice(reflect.SliceOf(t), 0, 0) 230 | } 231 | return items 232 | } 233 | 234 | func ConvertQueryTermToOrderTerm(term string) (fields []string, ascending []bool) { 235 | if strings.HasPrefix(term, "[") && strings.HasSuffix(term, "]") { 236 | term = term[1 : len(term)-1] 237 | } 238 | 239 | if term == "" { 240 | return 241 | } 242 | 243 | fieldConfigs := strings.Split(term, ",") 244 | 245 | for _, config := range fieldConfigs { 246 | if strings.HasSuffix(config, "-asc") { 247 | fields = append(fields, config[0:len(config)-4]) 248 | ascending = append(ascending, true) 249 | } else if strings.HasSuffix(config, "-desc") { 250 | fields = append(fields, config[0:len(config)-5]) 251 | ascending = append(ascending, false) 252 | } else { 253 | fields = append(fields, config) 254 | ascending = append(ascending, true) 255 | } 256 | } 257 | 258 | fmt.Println(ascending) 259 | 260 | return 261 | } 262 | 263 | func PtrEquals(ptr1 interface{}, ptr2 interface{}) bool { 264 | if reflect.TypeOf(ptr1).Kind() != reflect.Ptr { 265 | return false 266 | } 267 | if reflect.TypeOf(ptr2).Kind() != reflect.Ptr { 268 | return false 269 | } 270 | vptr1 := reflect.ValueOf(ptr1) 271 | vptr2 := reflect.ValueOf(ptr2) 272 | 273 | if vptr1.IsNil() { 274 | if vptr2.IsNil() { 275 | return true 276 | } 277 | return false 278 | } else if vptr2.IsNil() { 279 | return false 280 | } 281 | 282 | v1 := reflect.Indirect(vptr1) 283 | v2 := reflect.Indirect(vptr2) 284 | 285 | return v1.Interface() == v2.Interface() 286 | } 287 | 288 | func Unique(input []uint) []uint { 289 | u := make([]uint, 0, len(input)) 290 | m := make(map[uint]bool) 291 | 292 | for _, val := range input { 293 | if _, ok := m[val]; !ok { 294 | m[val] = true 295 | u = append(u, val) 296 | } 297 | } 298 | 299 | return u 300 | } 301 | -------------------------------------------------------------------------------- /util/helper_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | // Helper funcs 9 | 10 | type TestStruct struct { 11 | ID uint 12 | Title string 13 | Description *string 14 | } 15 | 16 | func AssertEqual(t *testing.T, expected, actual interface{}) { 17 | if !reflect.DeepEqual(expected, actual) { 18 | t.Errorf("assertEqual: Expected:\n%v\nbut got:\n%v\n (caller: %s)", expected, actual, CallerInfo()) 19 | } 20 | } 21 | 22 | func AssertTrue(t *testing.T, actual bool) { 23 | if !actual { 24 | t.Errorf("assertTrue: Assertion is false (caller: %s)", CallerInfo()) 25 | } 26 | } 27 | 28 | func AssertFalse(t *testing.T, actual bool) { 29 | if actual { 30 | t.Errorf("assertFalse: Assertion is true (caller: %s)", CallerInfo()) 31 | } 32 | } 33 | 34 | // Tests 35 | 36 | func TestAtou(t *testing.T) { 37 | var nilErr error 38 | 39 | val, err := Atou("1") 40 | AssertEqual(t, nilErr, err) 41 | AssertEqual(t, uint(1), val) 42 | 43 | val, err = Atou("0") 44 | AssertEqual(t, nilErr, err) 45 | AssertEqual(t, uint(0), val) 46 | 47 | val, err = Atou("1384234") 48 | AssertEqual(t, nilErr, err) 49 | AssertEqual(t, uint(1384234), val) 50 | 51 | val, err = Atou("-1") 52 | AssertEqual(t, nilErr, err) 53 | AssertEqual(t, uint(18446744073709551615), val) 54 | 55 | val, err = Atou("not a number") 56 | AssertEqual(t, "strconv.Atoi: parsing \"not a number\": invalid syntax", err.Error()) 57 | AssertEqual(t, uint(0), val) 58 | 59 | val, err = Atou("#234") 60 | AssertEqual(t, "strconv.Atoi: parsing \"#234\": invalid syntax", err.Error()) 61 | AssertEqual(t, uint(0), val) 62 | } 63 | 64 | func TestUtoa(t *testing.T) { 65 | AssertEqual(t, "1", Utoa(1)) 66 | AssertEqual(t, "0", Utoa(0)) 67 | AssertEqual(t, "1384234", Utoa(1384234)) 68 | } 69 | 70 | func TestCaptialize(t *testing.T) { 71 | AssertEqual(t, "Testing", Capitalize("testing")) 72 | AssertEqual(t, "This is a longer string", Capitalize("this is a longer string")) 73 | AssertEqual(t, "Already Capitalized", Capitalize("Already Capitalized")) 74 | AssertEqual(t, "", Capitalize("")) 75 | } 76 | 77 | func TestSnakeToCamel(t *testing.T) { 78 | AssertEqual(t, "Testing", SnakeToCamel("testing")) 79 | AssertEqual(t, "This is a longer string", SnakeToCamel("this is a longer string")) 80 | AssertEqual(t, "Already Capitalized", SnakeToCamel("Already Capitalized")) 81 | AssertEqual(t, "ActualSnake", SnakeToCamel("actual_snake")) 82 | AssertEqual(t, "ThisIsAMultipleSnake", SnakeToCamel("this_is_a_multiple_snake")) 83 | AssertEqual(t, "", SnakeToCamel("")) 84 | } 85 | 86 | func TestCamelToSnake(t *testing.T) { 87 | AssertEqual(t, "testing", CamelToSnake("Testing")) 88 | AssertEqual(t, "testing", CamelToSnake("testing")) 89 | AssertEqual(t, "actual_camel", CamelToSnake("ActualCamel")) 90 | AssertEqual(t, "this_is_a_multiple_camel", CamelToSnake("ThisIsAMultipleCamel")) 91 | AssertEqual(t, "", CamelToSnake("")) 92 | } 93 | 94 | func TestRound(t *testing.T) { 95 | AssertEqual(t, 5, Round(4.5)) 96 | AssertEqual(t, 4, Round(4.49)) 97 | AssertEqual(t, 5, Round(4.7)) 98 | AssertEqual(t, 5, Round(5.1)) 99 | AssertEqual(t, 5, Round(5.0)) 100 | AssertEqual(t, 0, Round(0.49)) 101 | AssertEqual(t, 0, Round(-0.49)) 102 | AssertEqual(t, -1, Round(-1.0)) 103 | AssertEqual(t, -2, Round(-1.5)) 104 | AssertEqual(t, -2, Round(-1.51)) 105 | } 106 | 107 | func TestRoundFloat(t *testing.T) { 108 | AssertEqual(t, 5.0, RoundFloat(4.5)) 109 | AssertEqual(t, 4.0, RoundFloat(4.49)) 110 | AssertEqual(t, 5.0, RoundFloat(4.7)) 111 | AssertEqual(t, 5.0, RoundFloat(5.1)) 112 | AssertEqual(t, 5.0, RoundFloat(5.0)) 113 | AssertEqual(t, 0.0, RoundFloat(0.49)) 114 | AssertEqual(t, 0.0, RoundFloat(-0.49)) 115 | AssertEqual(t, -1.0, RoundFloat(-1.0)) 116 | AssertEqual(t, -1.0, RoundFloat(-1.5)) 117 | AssertEqual(t, -2.0, RoundFloat(-1.51)) 118 | } 119 | 120 | func TestRoundToDecimal(t *testing.T) { 121 | AssertEqual(t, 5.0, RoundToDecimal(4.5, 0)) 122 | AssertEqual(t, 4.5, RoundToDecimal(4.5, 1)) 123 | AssertEqual(t, 4.5, RoundToDecimal(4.5, 2)) 124 | AssertEqual(t, 4.5, RoundToDecimal(4.49, 1)) 125 | AssertEqual(t, 4.49, RoundToDecimal(4.49, 2)) 126 | AssertEqual(t, 4.45, RoundToDecimal(4.449, 2)) 127 | AssertEqual(t, 12433.2342, RoundToDecimal(12433.2341698323239823, 4)) 128 | } 129 | 130 | func TestNanoToMicro(t *testing.T) { 131 | AssertEqual(t, int64(1452522568079810), NanoToMicro(1452522568079810000)) 132 | AssertEqual(t, int64(1452522568079810), NanoToMicro(1452522568079810260)) 133 | AssertEqual(t, int64(1452522568079811), NanoToMicro(1452522568079810500)) 134 | AssertEqual(t, int64(1452522568079810), NanoToMicro(1452522568079810499)) 135 | AssertEqual(t, int64(1452522568079811), NanoToMicro(1452522568079810599)) 136 | } 137 | 138 | func TestMicroToNano(t *testing.T) { 139 | AssertEqual(t, int64(1452522568079810000), MicroToNano(1452522568079810)) 140 | AssertEqual(t, int64(1452522568079811000), MicroToNano(1452522568079811)) 141 | } 142 | 143 | func TestIsZeroStruct(t *testing.T) { 144 | AssertEqual(t, false, IsZeroStruct(nil)) 145 | AssertEqual(t, true, IsZeroStruct(TestStruct{})) 146 | AssertEqual(t, true, IsZeroStruct(TestStruct{ID: 0})) 147 | AssertEqual(t, true, IsZeroStruct(TestStruct{ID: 0, Title: ""})) 148 | AssertEqual(t, false, IsZeroStruct(TestStruct{ID: 10})) 149 | AssertEqual(t, false, IsZeroStruct(TestStruct{Title: "Hello"})) 150 | s := "Hello, Test" 151 | AssertEqual(t, false, IsZeroStruct(TestStruct{Description: &s})) 152 | AssertEqual(t, false, IsZeroStruct(&TestStruct{})) 153 | var i interface{} 154 | AssertEqual(t, false, IsZeroStruct(i)) 155 | } 156 | 157 | func TestIsEmptyValue(t *testing.T) { 158 | AssertEqual(t, true, IsEmptyValue(reflect.ValueOf(0))) 159 | AssertEqual(t, false, IsEmptyValue(reflect.ValueOf(10))) 160 | AssertEqual(t, true, IsEmptyValue(reflect.ValueOf(""))) 161 | AssertEqual(t, false, IsEmptyValue(reflect.ValueOf("Yo mama"))) 162 | AssertEqual(t, true, IsEmptyValue(reflect.ValueOf(uint(0)))) 163 | AssertEqual(t, false, IsEmptyValue(reflect.ValueOf(uint(10)))) 164 | AssertEqual(t, true, IsEmptyValue(reflect.ValueOf(float64(0)))) 165 | AssertEqual(t, false, IsEmptyValue(reflect.ValueOf(float64(10)))) 166 | AssertEqual(t, true, IsEmptyValue(reflect.ValueOf([]uint{}))) 167 | AssertEqual(t, false, IsEmptyValue(reflect.ValueOf([]uint{1, 5, 7}))) 168 | var a, b *int 169 | bval := 10 170 | b = &bval 171 | AssertEqual(t, true, IsEmptyValue(reflect.ValueOf(a))) 172 | AssertEqual(t, false, IsEmptyValue(reflect.ValueOf(b))) 173 | } 174 | 175 | func TestPtrEquals(t *testing.T) { 176 | AssertFalse(t, PtrEquals(1, 1)) 177 | AssertFalse(t, PtrEquals(1, 2)) 178 | 179 | var s1, s2 string 180 | AssertFalse(t, PtrEquals(s1, s2)) 181 | s1 = "Hello" 182 | s2 = "World" 183 | AssertFalse(t, PtrEquals(s1, s2)) 184 | s2 = s1 185 | AssertFalse(t, PtrEquals(s1, s2)) 186 | 187 | var sPtr1, sPtr2 *string 188 | AssertTrue(t, PtrEquals(sPtr1, sPtr2)) 189 | sPtr1 = &s1 190 | AssertFalse(t, PtrEquals(sPtr1, sPtr2)) 191 | sPtr1 = nil 192 | sPtr2 = &s2 193 | AssertFalse(t, PtrEquals(sPtr1, sPtr2)) 194 | s2 = "World" 195 | sPtr1 = &s1 196 | sPtr2 = &s2 197 | AssertFalse(t, PtrEquals(sPtr1, sPtr2)) 198 | sPtr2 = &s1 199 | AssertTrue(t, PtrEquals(sPtr1, sPtr2)) 200 | s2 = "Hello" 201 | sPtr1 = &s1 202 | sPtr2 = &s2 203 | AssertTrue(t, PtrEquals(sPtr1, sPtr2)) 204 | 205 | var uPtr1, uPtr2 *uint 206 | u1 := uint(123) 207 | u2 := uint(345) 208 | AssertTrue(t, PtrEquals(uPtr1, uPtr2)) 209 | uPtr1 = &u1 210 | AssertFalse(t, PtrEquals(uPtr1, uPtr2)) 211 | uPtr1 = nil 212 | uPtr2 = &u2 213 | AssertFalse(t, PtrEquals(uPtr1, uPtr2)) 214 | uPtr1 = &u1 215 | uPtr2 = &u2 216 | AssertFalse(t, PtrEquals(uPtr1, uPtr2)) 217 | uPtr2 = &u1 218 | AssertTrue(t, PtrEquals(uPtr1, uPtr2)) 219 | u2 = uint(123) 220 | uPtr1 = &u1 221 | uPtr2 = &u2 222 | AssertTrue(t, PtrEquals(uPtr1, uPtr2)) 223 | } 224 | 225 | func TestUnique(t *testing.T) { 226 | integers := []uint{1, 2, 3, 3, 4, 5} 227 | unique := Unique(integers) 228 | expected := []uint{1, 2, 3, 4, 5} 229 | AssertEqual(t, unique, expected) 230 | } 231 | --------------------------------------------------------------------------------