All Projects → kuuland → kuu

kuuland / kuu

Licence: Apache-2.0 license
Modular Go Web Framework based on GORM and Gin.

Programming Languages

go
31211 projects - #10 most used programming language

Projects that are alternatives of or similar to kuu

Golang Gin Realworld Example App
Exemplary real world application built with Golang + Gin
Stars: ✭ 1,780 (+11766.67%)
Mutual labels:  gin, gorm
Cmall Go
golang写的电子商城的API接口
Stars: ✭ 167 (+1013.33%)
Mutual labels:  gin, gorm
Zendea
A free, open-source, self-hosted forum software written in Go 官方QQ群:656868
Stars: ✭ 116 (+673.33%)
Mutual labels:  gin, gorm
Ugin
UGin is an API boilerplate written in Go (Golang) with Gin Framework.
Stars: ✭ 110 (+633.33%)
Mutual labels:  gin, gorm
go api boilerplate
🐶Go (Golang)🚀REST / GraphQL API + Postgres boilerplate
Stars: ✭ 127 (+746.67%)
Mutual labels:  gin, gorm
Logrus
Hooks for logrus logging
Stars: ✭ 110 (+633.33%)
Mutual labels:  gin, gorm
Gosql
golang orm and sql builder
Stars: ✭ 141 (+840%)
Mutual labels:  gin, gorm
Wblog
基于gin+gorm开发的个人博客项目
Stars: ✭ 763 (+4986.67%)
Mutual labels:  gin, gorm
Goweibo
Go Weibo App
Stars: ✭ 243 (+1520%)
Mutual labels:  gin, gorm
Go init
一个用go组织项目结构,主要包括 gin, goredis, gorm, websocket, rabbitmq等。👉
Stars: ✭ 183 (+1120%)
Mutual labels:  gin, gorm
Ginbro
Converting a MySQL database'schema to a RESTful golang APIs app in the fastest way
Stars: ✭ 97 (+546.67%)
Mutual labels:  gin, gorm
pink-lady
a template project of gin app.
Stars: ✭ 44 (+193.33%)
Mutual labels:  gin, gorm
Duckygo
一个同时支持Session以及JWT的高性能高可用 Golang Restful API 脚手架 !
Stars: ✭ 57 (+280%)
Mutual labels:  gin, gorm
Ultimate Go
This repo contains my notes on working with Go and computer systems.
Stars: ✭ 1,530 (+10100%)
Mutual labels:  gin, gorm
Goforum
Let's go a forum
Stars: ✭ 23 (+53.33%)
Mutual labels:  gin, gorm
Gin bbs
Gin BBS App
Stars: ✭ 123 (+720%)
Mutual labels:  gin, gorm
Snake
🐍 一款小巧的基于Go构建的开发框架,可以快速构建API服务或者Web网站进行业务开发,遵循SOLID设计原则
Stars: ✭ 615 (+4000%)
Mutual labels:  gin, gorm
Go Gin Api
基于 Gin 进行模块化设计的 API 框架,封装了常用功能,使用简单,致力于进行快速的业务研发。比如,支持 cors 跨域、jwt 签名验证、zap 日志收集、panic 异常捕获、trace 链路追踪、prometheus 监控指标、swagger 文档生成、viper 配置文件解析、gorm 数据库组件、gormgen 代码生成工具、graphql 查询语言、errno 统一定义错误码、gRPC 的使用 等等。
Stars: ✭ 730 (+4766.67%)
Mutual labels:  gin, gorm
Goilerplate
Clean Boilerplate of Go, Domain-Driven Design, Clean Architecture, Gin and GORM.
Stars: ✭ 173 (+1053.33%)
Mutual labels:  gin, gorm
ginadmin
基于Gin开发的后台管理系统,集成了、数据库操作、日志管理、权限分配管理、多模板页面、自动分页器、数据库迁移和填充、Docker集成部署等功能、静态资源打包
Stars: ✭ 149 (+893.33%)
Mutual labels:  gin, gorm

Modular Go Web Framework

GoDoc Build Status codecov

Modular Go Web Framework based on GORM and Gin.

Contents

Installation

go get -u github.com/kuuland/kuu

Quick start

# assume the following codes in kuu.json file
$ cat kuu.json
{
  "prefix": "/api",
  "db": {
    "dialect": "postgres",
    "args": "host=127.0.0.1 port=5432 user=root dbname=kuu password=hello sslmode=disable"
  },
  "redis": {
    "addr": "127.0.0.1:6379"
  }
}
# assume the following codes in main.go file
$ cat main.go
package main

import (
	"github.com/gin-gonic/gin"
	_ "github.com/jinzhu/gorm/dialects/postgres"
	"github.com/kuuland/kuu"
)

func main() {
	r := kuu.Default()
	r.Import(kuu.Acc(), kuu.Sys())
	r.Run()
}
# run main.go and visit 0.0.0.0:8080 on browser
$ go run main.go

Features

Global configuration

# assume the following codes in kuu.json file
$ cat kuu.json
{
  "prefix": "/api",
  "cors": true,
  "gzip": true,
  "gorm:migrate": false,
  "db": {
    "dialect": "postgres",
    "args": "host=127.0.0.1 port=5432 user=root dbname=kuu password=hello sslmode=disable"
  },
  "redis": {
    "addr": "127.0.0.1:6379"
  },
  "statics": {
    "/assets": "assets/",
    "/drone_yml": ".drone.yml"
  }
}
func main() {
	kuu.C().Get("prefix")              // output "/api"
	kuu.C().GetBool("cors")            // output true
	kuu.C().GetBool("gorm:migrate")    // output true
    // load config from kuu.Param store on databases
	// only work when the Param.type is json and the Param.value is json object
    kuu.C().LoadFromParams("whitelist") // whitelist value is {"enable": true, "lable":"app", "items": ["item-1", "item-2"]}
	// and the key startwith: params
    kuu.C().GetBool("params.whilelist.enable") // output true
	kuu.C().GetString("params.whilelist.label") // output app
	var items []string
	kuu.C().GetInterface("params.whilelist.items", &items)
}

List of preset config:

  • prefix - Global routes prefix for kuu.Mod's Routes.
  • gorm:migrate - Enable GORM's auto migration for Mod's Models.
  • audit:callbacks - Register audit callbacks, default is true.
  • db - DB configs.
  • redis - Redis configs.
    • use kuu.GetRedisClient() get full redis client
  • cors - Attaches the official CORS gin's middleware.
  • gzip - Attaches the gin middleware to enable GZIP support.
  • statics - Static serves files from the given file system root or serve a single file.
  • whitelist:prefix - Let whitelist also matches paths with global prefix, default is true.
  • ignoreDefaultRootRoute - Do not mount the default root route, default is false.
  • logs - Log dir.

Notes: Static paths are automatically added to the whitelist.

Data Source Management

Single data source:

{
  "db": {
    "dialect": "postgres",
    "args": "host=127.0.0.1 port=5432 user=root dbname=db1 password=hello sslmode=disable"
  }
}
r.GET("/ping", func(c *kuu.Context) {
    var users []user
    kuu.DB().Find(&users)
    c.STD(&users)
})

Multiple data source:

{
  "db": [
    {
      "name": "ds1",
      "dialect": "postgres",
      "args": "host=127.0.0.1 port=5432 user=root dbname=db1 password=hello sslmode=disable"
    },
    {
      "name": "ds2",
      "dialect": "postgres",
      "args": "host=127.0.0.1 port=5432 user=root dbname=db1 password=hello sslmode=disable"
    }
  ]
}
r.GET("/ping", func(c *kuu.Context) {
    var users []user
    kuu.DB("ds1").Find(&users)
    c.STD(&users)
})

Use transaction

err := kuu.WithTransaction(func(tx *gorm.DB) error {
	// ...
    tx.Create(&memberDoc)
    if tx.NewRecord(memberDoc) {
        return errors.New("Failed to create member profile")
    }
    // ...
    tx.Create(...)
    return tx.Error
})

Notes: Remember to return tx.Error!!!

RESTful APIs for struct

Automatically mount RESTful APIs for struct:

type User struct {
	kuu.Model `rest:"*"`
	Code string
	Name string
}

func main() {
	kuu.RESTful(r, &User{})
}
[GIN-debug] POST   /api/user  --> github.com/kuuland/kuu.RESTful.func1 (4 handlers)
[GIN-debug] DELETE /api/user  --> github.com/kuuland/kuu.RESTful.func2 (4 handlers)
[GIN-debug] GET    /api/user  --> github.com/kuuland/kuu.RESTful.func3 (4 handlers)
[GIN-debug] PUT    /api/user  --> github.com/kuuland/kuu.RESTful.func4 (4 handlers)

On other fields:

type User struct {
	kuu.Model
	Code string `rest:"*"`
	Name string
}

You can also change the default request method:

type User struct {
	kuu.Model `rest:"C:POST;U:PUT;R:GET;D:DELETE"`
	Code string
	Name string
}

func main() {
	kuu.RESTful(r, &User{})
}

Or change route path:

type User struct {
	kuu.Model `rest:"*" route:"profile"`
	Code string
	Name string
}

func main() {
	kuu.RESTful(r, &User{})
}
[GIN-debug] POST   /api/profile  --> github.com/kuuland/kuu.RESTful.func1 (4 handlers)
[GIN-debug] DELETE /api/profile  --> github.com/kuuland/kuu.RESTful.func2 (4 handlers)
[GIN-debug] GET    /api/profile  --> github.com/kuuland/kuu.RESTful.func3 (4 handlers)
[GIN-debug] PUT    /api/profile  --> github.com/kuuland/kuu.RESTful.func4 (4 handlers)

Or unmount:

type User struct {
	kuu.Model `rest:"C:-;U:PUT;R:GET;D:-"` // unmount all: `rest:"-"`
	Code string
	Name string
}

func main() {
	kuu.RESTful(r, &User{})
}

Create Record

curl -X POST \
  http://localhost:8080/api/user \
  -H 'Content-Type: application/json' \
  -d '{
    "user": "test",
    "pass": "123"
}'

Batch Create

curl -X POST \
  http://localhost:8080/api/user \
  -H 'Content-Type: application/json' \
  -d '[
    {
        "user": "test1",
        "pass": "123456"
    },
    {
        "user": "test2",
        "pass": "123456"
    },
    {
        "user": "test3",
        "pass": "123456"
    }
]'

Query

Request querystring parameters:

curl -X GET \
  'http://localhost:8080/api/user?cond={"user":"test"}&sort=id&project=pass'
Key Desc Default Example
range data range, allow ALL and PAGE PAGE range=ALL
cond query condition, JSON string - cond={"user":"test"}
sort order fields - sort=id,-user
project select fields - project=user,pass
preload preload fields - preload=CreditCards,UserAddresses
export export data - export=true
page current page(required in PAGE mode) 1 page=2
size record size per page(required in PAGE mode) 30 size=100

Query operators:

Operator Desc Example
$regex LIKE cond={"user":{"$regex":"^test$"}}
$in IN cond={"id":{"$in":[1,2,5]}}
$nin NOT IN cond={"id":{"$nin":[1,2,5]}}
$eq Equal cond={"id":{"$eq":5}} equivalent to cond={"id":5}
$ne NOT Equal cond={"id":{"$ne":5}}
$exists IS NOT NULL cond={"pass":{"$exists":true}}
$gt Greater Than cond={"id":{"$gt":5}}
$gte Greater Than or Equal cond={"id":{"$gte":5}}
$lt Less Than cond={"id":{"$lt":20}}
$lte Less Than or Equal cond={"id":{"$lte":20}}, cond={"id":{"$gte":5,"$lte":20}}
$and AND cond={"user":"root","$and":[{"pass":"123"},{"pass":{"$regex":"^333"}}]}
$or OR cond={"user":"root","$or":[{"pass":"123"},{"pass":{"$regex":"^333"}}]}

Response JSON body:

{
    "data": {
        "cond": {
            "user": "test"
        },
        "list": [
            {
                "ID": 3,
                "CreatedAt": "2019-05-10T09:19:40.437816Z",
                "UpdatedAt": "2019-05-12T07:04:13.583093Z",
                "DeletedAt": null,
                "User": "test",
                "Pass": "123456"
            },
            {
                "ID": 5,
                "CreatedAt": "2019-05-10T10:31:43.203526Z",
                "UpdatedAt": "2019-05-12T07:04:13.583093Z",
                "DeletedAt": null,
                "User": "test",
                "Pass": "111222333"
            }
        ],
        "page": 1,
        "range": "PAGE",
        "size": 30,
        "sort": "id",
        "totalpages": 1,
        "totalrecords": 2
    },
    "code": 0,
    "msg": ""
}
Key Desc Default
list data list []
range data range, same as request PAGE
cond query condition, same as request -
sort order fields, same as request -
project select fields, same as request -
preload preload fields, same as request -
totalrecords total records 0
page current page, exist in PAGE mode 1
size record size per page, exist in PAGE mode 30
totalpages total pages, exist in PAGE mode 0

Update Fields

curl -X PUT \
  http://localhost:8080/api/user \
  -H 'Content-Type: application/json' \
  -d '{
    "cond": {
        "id": 5
    },
    "doc": {
        "user": "new username"
    }
}'

Notes: Pass only the fields that need to be updated!!!

Batch Updates

curl -X PUT \
  http://localhost:8080/api/user \
  -H 'Content-Type: application/json' \
  -d '{
    "cond": {
        "user": "test"
    },
    "doc": {
        "pass": "newpass"
    },
    "multi": true
}'

Notes: Pass only the fields that need to be updated!!!

Delete Record

curl -X DELETE \
  http://localhost:8080/api/user \
  -H 'Content-Type: application/json' \
  -d '{
    "cond": {
        "id": 5
    }
}'

Batch Delete

curl -X DELETE \
  http://localhost:8080/api/user \
  -H 'Content-Type: application/json' \
  -d '{
    "cond": {
        "user": "test"
    },
    "multi": true
}'

UnSoft Delete

curl -X DELETE \
  http://localhost:8080/api/user \
  -H 'Content-Type: application/json' \
  -d '{
    "cond": {
        "user": "test"
    },
    "unsoft": true
}'

Associations

Associations

  1. if association has a primary key, Kuu will call Update to save it, otherwise it will be created
  2. If the association has both ID and DeletedAt, Kuu will delete it.
  3. set "preload=field1,field2" to preload associations

Create associations

curl -X PUT \
  http://localhost:8080/api/user \
  -H 'Content-Type: application/json' \
  -d '{
    "cond": {
        "ID": 50
    },
    "doc": {
        "Emails": [
            {
                "Email": "[email protected]"
            },
            {
                "Email": "[email protected]"
            }
        ]
    }
}'

Update associations

ID is required:

curl -X PUT \
  http://localhost:8080/api/user \
  -d '{
    "cond": {
        "ID": 50
    },
    "doc": {
        "Emails": [
            {
                "ID": 101,
                "Email": "[email protected]"
            },
            {
                "ID": 159,
                "Email": "[email protected]"
            }
        ]
    }
}'

Notes: Pass only the fields that need to be updated!!!

Delete associations

ID and DeletedAt are required:

curl -X PUT \
  http://localhost:8080/api/user \
  -H 'Content-Type: application/json' \
  -d '{
    "cond": {
        "ID": 50
    },
    "doc": {
        "Emails": [
            {
                "ID": 101,
                "DeletedAt": "2019-06-25T17:05:06.000Z",
                "Email": "[email protected]"
            },
            {
                "ID": 159,
                "DeletedAt": "2019-06-25T17:05:06.000Z",
                "Email": "[email protected]"
            }
        ]
    }
}'

Notes: DeletedAt is only used as a flag, and will be re-assigned using server time when deleted.

Query associations

set "preload=Emails" to preload associations:

curl -X GET \
  'http://localhost:8080/api/user?cond={"ID":115}&preload=Emails'

Password field filter

type User struct {
	Model   `rest:"*" displayName:"用户" kuu:"password"`
	Username    string  `name:"账号"`
	Password    string  `name:"密码"`
}

users := []User{
    {Username: "root", Password: "xxx"},
    {Username: "admin", Password: "xxx"},
} 
users = kuu.Meta("User").OmitPassword(users) // => []User{ { Username: "root" }, { Username: "admin" } } 

Global default callbacks

You can override the default callbacks:

// Default create callback
kuu.CreateCallback = func(scope *gorm.Scope) {
	if !scope.HasError() {
		if desc := GetRoutinePrivilegesDesc(); desc != nil {
			var (
				hasOrgIDField       bool = false
				orgID               uint
				hasCreatedByIDField bool = false
				createdByID         uint
			)
			if field, ok := scope.FieldByName("OrgID"); ok {
				if field.IsBlank {
					if err := scope.SetColumn(field.DBName, desc.SignOrgID); err != nil {
						_ = scope.Err(fmt.Errorf("自动设置组织ID失败:%s", err.Error()))
						return
					}
				}
				hasOrgIDField = ok
				orgID = field.Field.Interface().(uint)
			}
			if field, ok := scope.FieldByName("CreatedByID"); ok {
				if err := scope.SetColumn(field.DBName, desc.UID); err != nil {
					_ = scope.Err(fmt.Errorf("自动设置创建人ID失败:%s", err.Error()))
					return
				}
				hasCreatedByIDField = ok
				createdByID = field.Field.Interface().(uint)
			}
			if field, ok := scope.FieldByName("UpdatedByID"); ok {
				if err := scope.SetColumn(field.DBName, desc.UID); err != nil {
					_ = scope.Err(fmt.Errorf("自动设置修改人ID失败:%s", err.Error()))
					return
				}
			}
			// 写权限判断
			if orgID == 0 {
				if hasCreatedByIDField && createdByID != desc.UID {
					_ = scope.Err(fmt.Errorf("用户 %d 只拥有个人可写权限", desc.UID))
					return
				}
			} else if hasOrgIDField && !desc.IsWritableOrgID(orgID) {
				_ = scope.Err(fmt.Errorf("用户 %d 在组织 %d 中无可写权限", desc.UID, orgID))
				return
			}
		}
	}
}


// Default delete callback
kuu.DeleteCallback = func(scope *gorm.Scope) {
	if !scope.HasError() {
		var extraOption string
		if str, ok := scope.Get("gorm:delete_option"); ok {
			extraOption = fmt.Sprint(str)
		}

		deletedAtField, hasDeletedAtField := scope.FieldByName("DeletedAt")
		var desc *PrivilegesDesc
		if desc = GetRoutinePrivilegesDesc(); desc != nil {
			AddDataScopeWritableSQL(scope, desc)
		}

		if !scope.Search.Unscoped && hasDeletedAtField {
			var sql string
			if desc != nil {
				deletedByField, hasDeletedByField := scope.FieldByName("DeletedByID")
				if !scope.Search.Unscoped && hasDeletedByField {
					sql = fmt.Sprintf(
						"UPDATE %v SET %v=%v,%v=%v%v%v",
						scope.QuotedTableName(),
						scope.Quote(deletedByField.DBName),
						scope.AddToVars(desc.UID),
						scope.Quote(deletedAtField.DBName),
						scope.AddToVars(gorm.NowFunc()),
						AddExtraSpaceIfExist(scope.CombinedConditionSql()),
						AddExtraSpaceIfExist(extraOption),
					)
				}
			}
			if sql == "" {
				sql = fmt.Sprintf(
					"UPDATE %v SET %v=%v%v%v",
					scope.QuotedTableName(),
					scope.Quote(deletedAtField.DBName),
					scope.AddToVars(gorm.NowFunc()),
					AddExtraSpaceIfExist(scope.CombinedConditionSql()),
					AddExtraSpaceIfExist(extraOption),
				)
			}
			scope.Raw(sql).Exec()
		} else {
			scope.Raw(fmt.Sprintf(
				"DELETE FROM %v%v%v",
				scope.QuotedTableName(),
				AddExtraSpaceIfExist(scope.CombinedConditionSql()),
				AddExtraSpaceIfExist(extraOption),
			)).Exec()
		}
		if scope.DB().RowsAffected < 1 {
			_ = scope.Err(errors.New("未删除任何记录,请检查更新条件或数据权限"))
			return
		}
	}
}

// Default update callback
kuu.UpdateCallback = func(scope *gorm.Scope) {
	if !scope.HasError() {
		if desc := GetRoutinePrivilegesDesc(); desc != nil {
			// 添加可写权限控制
			AddDataScopeWritableSQL(scope, desc)
			if err := scope.SetColumn("UpdatedByID", desc.UID); err != nil {
				ERROR("自动设置修改人ID失败:%s", err.Error())
			}
		}
	}
}

// Default query callback
kuu.QueryCallback = func(scope *gorm.Scope) {
	if !scope.HasError() {
		desc := GetRoutinePrivilegesDesc()
		if desc == nil {
			// 无登录登录态时
			return
		}

		caches := GetRoutineCaches()
		if caches != nil {
			// 有忽略标记时
			if _, ignoreAuth := caches[GLSIgnoreAuthKey]; ignoreAuth {
				return
			}
			// 查询用户菜单时
			if _, queryUserMenus := caches[GLSUserMenusKey]; queryUserMenus {
				if desc.NotRootUser() {
					_, hasCodeField := scope.FieldByName("Code")
					_, hasCreatedByIDField := scope.FieldByName("CreatedByID")
					if hasCodeField && hasCreatedByIDField {
						// 菜单数据权限控制与组织无关,且只有两种情况:
						// 1.自己创建的,一定看得到
						// 2.别人创建的,必须通过分配操作权限才能看到
						scope.Search.Where("(code in (?)) OR (created_by_id = ?)", desc.Codes, desc.UID)
					}
				}
				return
			}
		}
		AddDataScopeReadableSQL(scope, desc)
	}
}

// Default validate callback
kuu.ValidateCallback = func(scope *gorm.Scope) {
	if !scope.HasError() {
		if _, ok := scope.Get("gorm:update_column"); !ok {
			result, ok := scope.DB().Get(skipValidations)
			if !(ok && result.(bool)) {
				scope.CallMethod("Validate")
				if scope.Value == nil {
					return
				}
				resource := scope.IndirectValue().Interface()
				_, validatorErrors := govalidator.ValidateStruct(resource)
				if validatorErrors != nil {
					if errs, ok := validatorErrors.(govalidator.Errors); ok {
						for _, err := range FlatValidatorErrors(errs) {
							if err := scope.DB().AddError(formattedValidError(err, resource)); err != nil {
								ERROR("添加验证错误信息失败:%s", err.Error())
							}

						}
					} else {
						if err := scope.DB().AddError(validatorErrors); err != nil {
							ERROR("添加验证错误信息失败:%s", err.Error())
						}
					}
				}
			}
		}
	}
}

Inject custom authentication

Specify the token type, the default is ADMIN:

secret, err := kuu.GenToken(kuu.GenTokenDesc{
    Payload: jwt.MapClaims{
        "MemberID":  member.ID,
        "CreatedAt": member.CreatedAt,
    },
    UID:      member.UID,
    SubDocID: member.ID,
    Exp:      time.Now().Add(time.Second * time.Duration(kuu.ExpiresSeconds)).Unix(),
    Type:     "MY_SIGN_TYPE",
})

Inject your rules:

kuu.InjectCreateAuth = func(signType string, auth kuu.AuthProcessorDesc) (replace bool, err error) {
    return
}
kuu.InjectWritableAuth = func(signType string, auth kuu.AuthProcessorDesc) (replace bool, err error) {
    return
}
kuu.InjectReadableAuth = func(signType string, auth kuu.AuthProcessorDesc) (replace bool, err error) {
    return
}

Struct validation

base on govalidator:

// this struct definition will fail govalidator.ValidateStruct() (and the field values do not matter):
type exampleStruct struct {
  Name  string ``
  Email string `valid:"email"`
}

// this, however, will only fail when Email is empty or an invalid email address:
type exampleStruct2 struct {
  Name  string `valid:"-"`
  Email string `valid:"email"`
}

// lastly, this will only fail when Email is an invalid email address but not when it's empty:
type exampleStruct2 struct {
  Name  string `valid:"-"`
  Email string `valid:"email,optional"`
}

// Validate
func (e *exampleStruct2) Validate () error {
}

Modular project structure

Kuu will automatically mount routes, middlewares and struct RESTful APIs after Import:

type User struct {
	kuu.Model `rest`
	Username string
	Password string
}

type Profile struct {
	kuu.Model `rest`
	Nickname string
	Age int
}

func MyMod() *kuu.Mod {
	return &kuu.Mod{
		Models: []interface{}{
			&User{},
			&Profile{},
		},
		Middlewares: gin.HandlersChain{
			func(c *gin.Context) {
				// Auth middleware
			},
		},
		Routes: kuu.RoutesInfo{
			kuu.RouteInfo{
				Method: "POST",
				Path:   "/login",
				HandlerFunc: func(c *kuu.Context) {
					// POST /login
				},
			},
			kuu.RouteInfo{
				Method: "POST",
				Path:   "/logout",
				HandlerFunc: func(c *kuu.Context) {
					// POST /logout
				},
			},
		},
	}
}

func main() {
	r := kuu.Default()
	r.Import(kuu.Acc(), kuu.Sys())     // import preset modules
	r.Import(MyMod())                       // import custom module
}

Global log API

func main() {
	kuu.PRINT("Hello Kuu")  // PRINT[0000] Hello Kuu
	kuu.DEBUG("Hello Kuu")  // DEBUG[0000] Hello Kuu
	kuu.WARN("Hello Kuu")   // WARN[0000] Hello Kuu
	kuu.INFO("Hello Kuu")   // INFO[0000] Hello Kuu
	kuu.FATAL("Hello Kuu")  // FATAL[0000] Hello Kuu
	kuu.PANIC("Hello Kuu")  // PANIC[0000] Hello Kuu
}

Or with params:

func main() {
	kuu.INFO("Hello %s", "Kuu")  // INFO[0000] Hello Kuu
}

Standard response format

func main() {
	r := kuu.Default()
	r.GET("/ping", func(c *kuu.Context) {
        c.STD("hello")                                  // response: {"data":"hello","code":0}
        c.STD("hello", c.L("ping_success", "Success"))  // response: {"data":"hello","code":0,"msg":"Success"}
        c.STD(1800)                                     // response: {"data":1800,"code":0}
        c.STDErr(c.L("ping_failed_new", "New record failed"))       // response: {"code":-1,"msg":"New record failed"}
        c.STDErr(c.L("ping_failed_new", "New record failed"), err)  // response: {"code":-1,"msg":"New record failed","data":"错误详细描述信息,对应err.Error()"}
        c.STDErrHold(c.L("ping_failed_token", "Token decoding failed"), errors.New("token not found")).Code(555).Render() // response: {"code":555,"msg":"Token decoding failed","data":"token not found"}
    })
}

Notes:

  • If data == error, Kuu will call ERROR(data) to output the log.
  • All message will call kuu.L(c, msg) for i18n before the response.

Get login context

r.GET(func (c *kuu.Context){
	c.SignInfo // Login user info
	c.PrisDesc // Login user privileges
})

Goroutine local storage

// preset caches
kuu.GetRoutinePrivilegesDesc()
kuu.GetRoutineValues()
kuu.GetRoutineRequestContext()

// custom caches
kuu.GetRoutineCaches()
kuu.SetRoutineCache(key, value)
kuu.GetRoutineCache(key)
kuu.DelRoutineCache(key)

// Ignore default data filters
kuu.IgnoreAuth() // Equivalent to c.IgnoreAuth/kuu.GetRoutineValues().IgnoreAuth

Whitelist

All routes are blocked by the authentication middleware by default. If you want to ignore some routes, please configure the whitelist:

kuu.AddWhitelist("GET /", "GET /user")
kuu.AddWhitelist(regexp.MustCompile("/user"))

Notes: Whitelist also matches paths with global prefix. If you don't want this feature, please set "whitelist:prefix":false.

Hooks

Rest api hooks

  • hooks keys format: StructName:Operation, operation list:
    • BizBeforeFind
    • BizAfterFind
    • BizBeforeCreate
    • BizAfterCreate
    • BizBeforeUpdate
    • BizAfterUpdate
    • BizBeforeDelete
    • BizAfterDelete
kuu.RegisterBizHook("User:BizBeforeCreate", func (scope *kuu.Scope) error {
    db := scope.DB // db instance
    user, ok := scope.Value.(*User)
	fmt.Println(user, ok)
    return nil
})

Gorm global hooks

Mainly used to solve circular dependency problems

  • is same as gorm hooks, the hooks key format: StructName:Operation, operation list:
    • BeforeSave
    • BeforeCreate
    • BeforeUpdate
    • AfterUpdate
    • AfterSave
    • AfterCreate
kuu.RegisterGormHook("User:BeforeSave", func(scope *gorm.Scope) error {
    return nil
})

i18n

kuu.L("acc_logout_failed", "Logout failed").Render()                             // => Logout failed
kuu.L("fano_table_total", "Total {{total}} items", kuu.M{"total": 500}).Render() // => Total 500 items
  • Use a unique key
  • Always set defaultMessage

Best Practices

func singleMessage(c *kuu.Context) {
	failedMessage := c.L("import_failed", "Import failed")
	file, _ := c.FormFile("file")
	if file == nil {
		c.STDErr(failedMessage, errors.New("no 'file' key in form-data"))
		return
	}
	src, err := file.Open()
	if err != nil {
		c.STDErr(failedMessage, err)
		return
	}
	defer src.Close()
}

func multiMessage(c *kuu.Context) {
	var (
		phoneIncorrect = c.L("phone_incorrect", "Phone number is incorrect")
		passwordIncorrect = c.L("password_incorrect", "The password is incorrect")
	)
	if err := checkPhoneNumber(...); err != nil {
		c.STDErr(phoneIncorrect, err)
		return
	}
	if err := checkPassword(...); err != nil {
		c.STDErr(passwordIncorrect, err)
		return
	}
	c.STD(...)
}

Manual Registration

register := kuu.NewLangRegister(kuu.DB())
register.SetKey("acc_please_login").Add("Please login", "请登录", "請登錄")
register.SetKey("auth_failed").Add("Authentication failed", "鉴权失败", "鑒權失敗")
register.SetKey("acc_logout_failed").Add("Logout failed", "登出失败", "登出失敗")
register.SetKey("kuu_welcome").Add("Welcome {{name}}", "欢迎{{name}}", "歡迎{{name}}")

Common utils

func main() {
	r := kuu.Default()
	// Parse JSON from string
	var params map[string]string
	kuu.Parse(`{"user":"kuu","pass":"123"}`, &params)
	// Formatted as JSON
	kuu.Stringify(&params)
}
  • IsBlank - Check if value is empty
  • Stringify - Converts value to a JSON string
  • Parse - Parses a JSON string to the value
  • EnsureDir - Ensures that the directory exists
  • Copy - Copy values
  • RandCode - Generate random code
  • If - Conditional expression

Preset modules

Security framework

Kuu Security framework

FAQ

Why called Kuu?

Kuu and Shino

Kuu and Shino

License

Kuu is available under the Apache License, Version 2.0.

Thanks to

  • JetBrains for their open source license(s).

JetBrains

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].