通过上次的QOR实作,感觉QOR对后台的搭建还是非常快的。常用的过滤、增删改查都比较方便。
但是它使用了GORM的1.0版本,而它的2.0版更新较大。简单的修改已不能将QOR升级到GORM2了。
另一方面,也需要对QOR的整体作更详细的了解,便于更多QOR库的使,扩充它的功能。
今天对基础库作了一个简单了解。所有的扩充功能都在它的基础之上进行,例如最常用的admin 后台管理。
“啃”代码是痛苦的,一步步来。
这里截取一些聊天室的内容,希望对读代码有帮助。或许有些已经老久,经历了版本更替。
QOR的中文聊天室是2015.7.23 11:43开通的,别问我为什么知道。
看起来QOR与GORM的作者是同一人?
2016.1.14 jinzhu老婆快生了…. :)
Raven
https://github.com/qor/gomerchant 抽象支付接口
https://github.com/qor/auth 用于 Web 开发的模块化身份验证系统
resource在 qor 里的地位挺重要的,我们把所有的 struct 都当成一个资源,然后将这个资源通过admin 来管理resource 的管理大概上来说就是 CURD 这四个部分 然后 resource 有四个接口来分别对应 CURD 这四个功能qor 给 resource 通过gorm默认实现了CURD这四个功能来对应数据库,不过你也可以重写,例如通过 qor 的 admin 从 API 读取,重新数据之类的
其中涉及到GORM旧版的有几个文件:
config.go 较为简单
context.go 较为简单
resource/crud.go
resource/meta.go
resource/processor.go
utils/utils.go
https://github.com/qor/qor.git
结构
├─resource
│ ├─ crud.go
│ ├─ meta.go
│ ├─ meta_value.go
│ ├─ processor.go
│ ├─ resource.go
│ └─ schema.go
├─utils
│ ├─ buffer.go
│ ├─ meta.go
│ ├─ params.go
│ └─ utils.go
├─ config.go
├─ context.go
└─ errors.go
config.go
定义了Config结构体,用于保存数据库对象
type Config struct {
DB *gorm.DB
}
context.go
涉及旧版GORM,修改简单
CurrentUser接口,获取当前登录的用户
type CurrentUser interface {
DisplayName() string
}
QOR上下文,用于在各个QOR组件中共享信息
type Context struct {
Request *http.Request // 请求句柄
Writer http.ResponseWriter // 输出句柄
CurrentUser CurrentUser // 当前用户
Roles []string // 权限
ResourceID string // 资源ID
DB *gorm.DB // 数据库句柄
Config *Config // 配置句柄
Errors // 错误结构(保存错误信息列表)
}
克隆当前上下文
func (context *Context) Clone() *Context {
var clone = *context
return &clone
}
从当前上下文中获取数据库句柄
func (context *Context) GetDB() *gorm.DB {
if context.DB != nil {
return context.DB
}
return context.Config.DB
}
设置上下文中的数据库句柄
func (context *Context) SetDB(db *gorm.DB) {
context.DB = db
}
errors.go
用于保存Errors数组的结构体
type Errors struct {
errors []error
}
格式化错误消息 多个错误消息用;号分隔为一个字符串
func (errs Errors) Error() string {
var errors []string
for _, err := range errs.errors {
errors = append(errors, err.Error())
}
return strings.Join(errors, "; ")
}
添加错误消息到Errors数组中
func (errs *Errors) AddError(errors ...error) {
for _, err := range errors {
if err != nil {
if e, ok := err.(errorsInterface); ok {
errs.errors = append(errs.errors, e.GetErrors()...)
} else {
errs.errors = append(errs.errors, err)
}
}
}
}
是否有错误信息
func (errs Errors) HasError() bool {
return len(errs.errors) != 0
}
返回保存的错误信息数组
func (errs Errors) GetErrors() []error {
return errs.errors
}
错误接口,获取当前错误信息数组
type errorsInterface interface {
GetErrors() []error
}
utils/buffer.go
实现Closer接口的ReadSeeker方法(即可以定位,又可读取) ReadSeeker 是 Read 和 Seek 方法的组合 io.Seeker方法用于指定下次读取或者写入时的偏移量 io.Reader接口定义了 Read 方法,用于读取数据到字节数组中
type ClosingReadSeeker struct {
io.ReadSeeker
}
实现关闭接口
func (ClosingReadSeeker) Close() error {
return nil
}
utils/meta.go
均为一些结构的转换
具有反射类型的新结构值 TODO: 作用待研究
func NewValue(t reflect.Type) (v reflect.Value) {
v = reflect.New(t)
ov := v
for t.Kind() == reflect.Ptr {
v = v.Elem()
t = t.Elem()
e := reflect.New(t)
v.Set(e)
}
if e := v.Elem(); e.Kind() == reflect.Map && e.IsNil() {
v.Elem().Set(reflect.MakeMap(v.Elem().Type()))
}
return ov
}
从value中获取数组,将忽略空白字符串将其转换为数组
func ToArray(value interface{}) (values []string) {
switch value := value.(type) {
case []string:
values = []string{}
for _, v := range value {
if v != "" {
values = append(values, v)
}
}
case []interface{}:
for _, v := range value {
values = append(values, fmt.Sprint(v))
}
default:
if value := fmt.Sprint(value); value != "" {
values = []string{value}
}
}
return
}
从值获取字符串,如果传递的值是一个切片,将使用第一个元素
func ToString(value interface{}) string {
if v, ok := value.([]string); ok {
for _, s := range v {
if s != "" {
return s
}
}
return ""
} else if v, ok := value.(string); ok {
return v
} else if v, ok := value.([]interface{}); ok {
for _, s := range v {
if fmt.Sprint(s) != "" {
return fmt.Sprint(s)
}
}
return ""
}
return fmt.Sprintf("%v", value)
}
从值中获取int,如果传递的值为空字符串,结果将为0
func ToInt(value interface{}) int64 {
if result := ToString(value); result == "" {
return 0
} else if i, err := strconv.ParseInt(result, 10, 64); err == nil {
return i
} else {
panic("failed to parse int: " + result)
}
}
从值中获取uint,如果传递的值为空字符串,结果将为0
func ToUint(value interface{}) uint64 {
if result := ToString(value); result == "" {
return 0
} else if i, err := strconv.ParseUint(result, 10, 64); err == nil {
return i
} else {
panic("failed to parse uint: " + result)
}
}
从值中获取浮点值,若传递的值为空字符串,则结果为0
func ToFloat(value interface{}) float64 {
if result := ToString(value); result == "" {
return 0
} else if i, err := strconv.ParseFloat(result, 64); err == nil {
return i
} else {
panic("failed to parse float: " + result)
}
}
utils/params.go
isAlpha 是否为字母和_-!字符 isDigit 是否为数字 matchPart 两个byte是否不同,且不为/
func matchPart(b byte) func(byte) bool {
return func(c byte) bool {
return c != b && c != '/'
}
}
match ?
func match(s string, f func(byte) bool, i int) (matched string, next byte, j int) {
j = i
for j < len(s) && f(s[j]) {
j++
}
if j < len(s) {
next = s[j]
}
return s[i:j], next, j
}
ParamsMatch 按参数匹配字符串 根据访问路径,返回参数字典及主路径 TODO: 未读完,在调用中去了解
// url.Values -> map[string][]string
func ParamsMatch(source string, pth string) (url.Values, string, bool) {
var (
i, j int
p = make(url.Values)
ext = path.Ext(pth) // 获取路径中的扩展名
)
pth = strings.TrimSuffix(pth, ext) // 路径中清除扩展名部份
// 添加map主键 p[":format"]= ext , 删除扩展名中可能存在的.号
if ext != "" {
p.Add(":format", strings.TrimPrefix(ext, "."))
}
// 循环,直到路径长度小于i
for i < len(pth) {
switch {
case j >= len(source):
// source不为/,且不为空,且最后一个字符为/
// 返回 参数字典,去参数主路径?
if source != "/" && len(source) > 0 && source[len(source)-1] == '/' {
return p, pth[:i], true
}
// source为空,且路径为/
// 返回参数字典和路径/
if source == "" && pth == "/" {
return p, pth, true
}
return p, pth[:i], false
case source[j] == ':':
var name, val string
var nextc byte
name, nextc, j = match(source, isAlnum, j+1)
val, _, i = match(pth, matchPart(nextc), i)
if (j < len(source)) && source[j] == '[' {
var index int
if idx := strings.Index(source[j:], "]/"); idx > 0 {
index = idx
} else if source[len(source)-1] == ']' {
index = len(source) - j - 1
}
if index > 0 {
match := strings.TrimSuffix(strings.TrimPrefix(source[j:j+index+1], "["), "]")
if reg, err := regexp.Compile("^" + match + "$"); err == nil && reg.MatchString(val) {
j = j + index + 1
} else {
return nil, "", false
}
}
}
p.Add(":"+name, val)
case pth[i] == source[j]:
i++
j++
default:
return nil, "", false
}
}
if j != len(source) {
if (len(source) == j+1) && source[j] == '/' {
return p, pth, true
}
return nil, "", false
}
return p, pth, true
}
utils/utils.go
涉及旧版GORM
var AppRoot, _ = os.Getwd() // 应用的根路径
type ContextKey string // 定义一个类型,用于上下文
var ContextDBName ContextKey = "ContextDB" // 上下文:数据库名
var HTMLSanitizer = bluemonday.UGCPolicy() // html中有害内容的清除,防止跨站攻击。这是通过 https://github.com/microcosm-cc/bluemonday 实现的
func init() {
HTMLSanitizer.AllowStandardAttributes() // 过滤时,允许标准的属性:"dir", "id", "lang", "title"
if path := os.Getenv("WEB_ROOT"); path != "" { // 可通过环境变量WEB_ROOT设置应用路径,不设置默认为当前路径
AppRoot = path
}
}
返回GOPATH设置
func GOPATH() []string {
paths := strings.Split(os.Getenv("GOPATH"), string(os.PathListSeparator))
if len(paths) == 0 {
fmt.Println("GOPATH doesn't exist")
}
return paths
}
从请求中获取数据库
var GetDBFromRequest = func(req *http.Request) *gorm.DB {
db := req.Context().Value(ContextDBName)
if tx, ok := db.(*gorm.DB); ok {
return tx
}
return nil
}
人性化字符串 “OrderItem” -> “Order Item” 基于大写字母,将其分隔
func HumanizeString(str string) string {
var human []rune
for i, l := range str {
if i > 0 && isUppercase(byte(l)) {
if (!isUppercase(str[i-1]) && str[i-1] != ' ') || (i+1 < len(str) && !isUppercase(str[i+1]) && str[i+1] != ' ' && str[i-1] != ' ') {
human = append(human, rune(' '))
}
}
human = append(human, l)
}
return strings.Title(string(human))
}
是否为大写字母
func isUppercase(char byte) bool {
return 'A' <= char && char <= 'Z'
}
ASCII匹配正则表达式 var asicsiiRegexp = regexp.MustCompile("^(\w|\s|-|!)*$")
用下划线替换空格和分隔单词,并用小写字母替换大写 ToParamString -> to_param_string, To ParamString -> to_param_string
func ToParamString(str string) string {
if asicsiiRegexp.MatchString(str) {
// 这里使用了gorm的方法,在升级时需替换
// gorm.ToDBName 将字符串转换为数据库名称
return gorm.ToDBName(strings.Replace(str, " ", "_", -1))
}
// github.com/gosimple/slug 将各种语言转换为英文/字母方式的字符串
// slug.LowerCase = false 则字母大写化
return slug.Make(str)
}
参数与URL合成 PatchURL(“google.com”,“key”,“value”) => “google.com?key=value”
func PatchURL(originalURL string, params ...interface{}) (patchedURL string, err error) {
url, err := url.Parse(originalURL)
if err != nil {
return
}
query := url.Query()
for i := 0; i < len(params)/2; i++ {
// Check if params is key&value pair
key := fmt.Sprintf("%v", params[i*2])
value := fmt.Sprintf("%v", params[i*2+1])
if value == "" {
query.Del(key)
} else {
query.Set(key, value)
}
}
url.RawQuery = query.Encode()
patchedURL = url.String()
return
}
将字符串加入请求路径中 JoinURL(“google.com”, “admin”) => “google.com/admin” JoinURL(“google.com?q=keyword”, “admin”) => “google.com/admin?q=keyword”
func JoinURL(originalURL string, paths ...interface{}) (joinedURL string, err error) {
u, err := url.Parse(originalURL)
if err != nil {
return
}
var urlPaths = []string{u.Path}
for _, p := range paths {
urlPaths = append(urlPaths, fmt.Sprint(p))
}
if strings.HasSuffix(strings.Join(urlPaths, ""), "/") {
u.Path = path.Join(urlPaths...) + "/"
} else {
u.Path = path.Join(urlPaths...)
}
joinedURL = u.String()
return
}
上下文中设置cookie
func SetCookie(cookie http.Cookie, context *qor.Context) {
cookie.HttpOnly = true
// set https cookie
if context.Request != nil && context.Request.URL.Scheme == "https" {
cookie.Secure = true
}
// set default path
if cookie.Path == "" {
cookie.Path = "/"
}
http.SetCookie(context.Writer, &cookie)
}
对象转字符串 ?? 如果它是一个结构,将尝试使用其名称、标题、代码字段,否则将使用其主键
func Stringify(object interface{}) string {
if obj, ok := object.(interface {
Stringify() string
}); ok {
return obj.Stringify()
}
// 这里调用gorm,生成一个Scope
scope := gorm.Scope{Value: object}
for _, column := range []string{"Name", "Title", "Code"} {
// scope.FieldByName 返回字段名或数据库名
if field, ok := scope.FieldByName(column); ok {
if field.Field.IsValid() {
result := field.Field.Interface()
if valuer, ok := result.(driver.Valuer); ok {
if result, err := valuer.Value(); err == nil {
return fmt.Sprint(result)
}
}
return fmt.Sprint(result)
}
}
}
if scope.PrimaryField() != nil {
if scope.PrimaryKeyZero() {
return ""
}
return fmt.Sprintf("%v#%v", scope.GetModelStruct().ModelType.Name(), scope.PrimaryKeyValue())
}
return fmt.Sprint(reflect.Indirect(reflect.ValueOf(object)).Interface())
}
获取value的模型类型
func ModelType(value interface{}) reflect.Type {
reflectType := reflect.Indirect(reflect.ValueOf(value)).Type()
for reflectType.Kind() == reflect.Ptr || reflectType.Kind() == reflect.Slice {
reflectType = reflectType.Elem()
}
return reflectType
}
将标记选项解析为map
func ParseTagOption(str string) map[string]string {
tags := strings.Split(str, ";")
setting := map[string]string{}
for _, value := range tags {
v := strings.Split(value, ":")
k := strings.TrimSpace(strings.ToUpper(v[0]))
if len(v) == 2 {
setting[k] = v[1]
} else {
setting[k] = k
}
}
return setting
}
调试错误消息和打印堆栈
func ExitWithMsg(msg interface{}, value ...interface{}) {
fmt.Printf("\n"+filenameWithLineNum()+"\n"+fmt.Sprint(msg)+"\n", value...)
debug.PrintStack()
}
禁用文件列表的文件服务器
func FileServer(dir http.Dir) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := path.Join(string(dir), r.URL.Path)
if f, err := os.Stat(p); err == nil && !f.IsDir() {
http.ServeFile(w, r, p)
return
}
http.NotFound(w, r)
})
}
供出错信息调用,显示文件名和行号 通过runtime.Caller获取
func filenameWithLineNum() string {
var total = 10
var results []string
for i := 2; i < 15; i++ {
if _, file, line, ok := runtime.Caller(i); ok {
total--
results = append(results[:0],
append(
[]string{fmt.Sprintf("%v:%v", strings.TrimPrefix(file, os.Getenv("GOPATH")+"src/"), line)},
results[0:]...)...)
if total == 0 {
return strings.Join(results, "\n")
}
}
}
return ""
}
// utils.GetLocale = func(context *qor.Context) string { // // …. // } 从请求中获取语言环境,获取语言环境后,写入cookie
var GetLocale = func(context *qor.Context) string {
if locale := context.Request.Header.Get("Locale"); locale != "" {
return locale
}
if locale := context.Request.URL.Query().Get("locale"); locale != "" {
if context.Writer != nil {
context.Request.Header.Set("Locale", locale)
SetCookie(http.Cookie{Name: "locale", Value: locale, Expires: time.Now().AddDate(1, 0, 0)}, context)
}
return locale
}
if locale, err := context.Request.Cookie("locale"); err == nil {
return locale.Value
}
return ""
}
// Overwrite the default logic with // utils.ParseTime = func(timeStr string, context *qor.Context) (time.Time, error) { // // …. // } 从字符串解析时间
var ParseTime = func(timeStr string, context *qor.Context) (time.Time, error) {
return now.Parse(timeStr)
}
// Overwrite the default logic with // utils.FormatTime = func(time time.Time, format string, context *qor.Context) string { // // …. // } 格式化时间为字符串
var FormatTime = func(date time.Time, format string, context *qor.Context) string {
return date.Format(format)
}
var replaceIdxRegexp = regexp.MustCompile(\[\d+\]
)
// 表单关键词排序
func SortFormKeys(strs []string) {
sort.Slice(strs, func(i, j int) bool { // true for first
str1 := strs[i]
str2 := strs[j]
matched1 := replaceIdxRegexp.FindAllStringIndex(str1, -1)
matched2 := replaceIdxRegexp.FindAllStringIndex(str2, -1)
for x := 0; x < len(matched1); x++ {
prefix1 := str1[:matched1[x][0]]
prefix2 := str2
if len(matched2) >= x+1 {
prefix2 = str2[:matched2[x][0]]
}
if prefix1 != prefix2 {
return strings.Compare(prefix1, prefix2) < 0
}
if len(matched2) < x+1 {
return false
}
number1 := str1[matched1[x][0]:matched1[x][1]]
number2 := str2[matched2[x][0]:matched2[x][1]]
if number1 != number2 {
if len(number1) != len(number2) {
return len(number1) < len(number2)
}
return strings.Compare(number1, number2) < 0
}
}
return strings.Compare(str1, str2) < 0
})
}
refer: https://stackoverflow.com/questions/6899069/why-are-request-url-host-and-scheme-blank-in-the-development-server 从请求中获取绝对URL
func GetAbsURL(req *http.Request) url.URL {
if req.URL.IsAbs() {
return *req.URL
}
var result *url.URL
if domain := req.Header.Get("Origin"); domain != "" {
result, _ = url.Parse(domain)
} else {
if req.TLS == nil {
result, _ = url.Parse("http://" + req.Host)
} else {
result, _ = url.Parse("https://" + req.Host)
}
}
result.Parse(req.RequestURI)
return *result
}
// 返回v指向的最后一个值
func Indirect(v reflect.Value) reflect.Value {
for v.Kind() == reflect.Ptr {
v = reflect.Indirect(v)
}
return v
}
删除给定切片中的重复值
func SliceUniq(s []string) []string {
for i := 0; i < len(s); i++ {
for i2 := i + 1; i2 < len(s); i2++ {
if s[i] == s[i2] {
// delete
s = append(s[:i2], s[i2+1:]...)
i2--
}
}
}
return s
}
https://snyk.io/research/zip-slip-vulnerability#go 安全连接
func SafeJoin(paths ...string) (string, error) {
result := path.Join(paths...)
// check filepath
if !strings.HasPrefix(result, filepath.Clean(paths[0])+string(os.PathSeparator)) {
return "", errors.New("invalid filepath")
}
return result, nil
}
resource/crud.go
涉及旧版GORM
调用FindOne方法
func (res *Resource) CallFindOne(result interface{}, metaValues *MetaValues, context *qor.Context) error {
return res.FindOneHandler(result, metaValues, context)
}
调用FindMany方法
func (res *Resource) CallFindMany(result interface{}, context *qor.Context) error {
return res.FindManyHandler(result, context)
}
调用保存方法
func (res *Resource) CallSave(result interface{}, context *qor.Context) error {
return res.SaveHandler(result, context)
}
调用删除方法
func (res *Resource) CallDelete(result interface{}, context *qor.Context) error {
return res.DeleteHandler(result, context)
}
基于主键生成查询参数,多个主键值用逗号链接
func (res *Resource) ToPrimaryQueryParams(primaryValue string, context *qor.Context) (string, []interface{}) {
if primaryValue != "" {
scope := context.GetDB().NewScope(res.Value)
// multiple primary fields
if len(res.PrimaryFields) > 1 {
if primaryValueStrs := strings.Split(primaryValue, ","); len(primaryValueStrs) == len(res.PrimaryFields) {
sqls := []string{}
primaryValues := []interface{}{}
for idx, field := range res.PrimaryFields {
sqls = append(sqls, fmt.Sprintf("%v.%v = ?", scope.QuotedTableName(), scope.Quote(field.DBName)))
primaryValues = append(primaryValues, primaryValueStrs[idx])
}
return strings.Join(sqls, " AND "), primaryValues
}
}
// fallback to first configured primary field
if len(res.PrimaryFields) > 0 {
return fmt.Sprintf("%v.%v = ?", scope.QuotedTableName(), scope.Quote(res.PrimaryFields[0].DBName)), []interface{}{primaryValue}
}
// if no configured primary fields found
if primaryField := scope.PrimaryField(); primaryField != nil {
return fmt.Sprintf("%v.%v = ?", scope.QuotedTableName(), scope.Quote(primaryField.DBName)), []interface{}{primaryValue}
}
}
return "", []interface{}{}
}
基于元值生成查询参数
func (res *Resource) ToPrimaryQueryParamsFromMetaValue(metaValues *MetaValues, context *qor.Context) (string, []interface{}) {
var (
sqls []string
primaryValues []interface{}
scope = context.GetDB().NewScope(res.Value)
)
if metaValues != nil {
for _, field := range res.PrimaryFields {
if metaField := metaValues.Get(field.Name); metaField != nil {
sqls = append(sqls, fmt.Sprintf("%v.%v = ?", scope.QuotedTableName(), scope.Quote(field.DBName)))
primaryValues = append(primaryValues, utils.ToString(metaField.Value))
}
}
}
return strings.Join(sqls, " AND "), primaryValues
}
func (res *Resource) findOneHandler(result interface{}, metaValues *MetaValues, context *qor.Context) error {
if res.HasPermission(roles.Read, context) {
var (
primaryQuerySQL string
primaryParams []interface{}
)
if metaValues == nil {
primaryQuerySQL, primaryParams = res.ToPrimaryQueryParams(context.ResourceID, context)
} else {
primaryQuerySQL, primaryParams = res.ToPrimaryQueryParamsFromMetaValue(metaValues, context)
}
if primaryQuerySQL != "" {
if metaValues != nil {
if destroy := metaValues.Get("_destroy"); destroy != nil {
if fmt.Sprint(destroy.Value) != "0" && res.HasPermission(roles.Delete, context) {
context.GetDB().Delete(result, append([]interface{}{primaryQuerySQL}, primaryParams...)...)
return ErrProcessorSkipLeft
}
}
}
return context.GetDB().First(result, append([]interface{}{primaryQuerySQL}, primaryParams...)...).Error
}
return errors.New("failed to find")
}
return roles.ErrPermissionDenied
}
func (res *Resource) findManyHandler(result interface{}, context *qor.Context) error {
if res.HasPermission(roles.Read, context) {
db := context.GetDB()
if _, ok := db.Get("qor:getting_total_count"); ok {
return context.GetDB().Count(result).Error
}
return context.GetDB().Set("gorm:order_by_primary_key", "DESC").Find(result).Error
}
return roles.ErrPermissionDenied
}
func (res *Resource) saveHandler(result interface{}, context *qor.Context) error {
if (context.GetDB().NewScope(result).PrimaryKeyZero() &&
res.HasPermission(roles.Create, context)) || // has create permission
res.HasPermission(roles.Update, context) { // has update permission
return context.GetDB().Save(result).Error
}
return roles.ErrPermissionDenied
}
func (res *Resource) deleteHandler(result interface{}, context *qor.Context) error {
if res.HasPermission(roles.Delete, context) {
if primaryQuerySQL, primaryParams := res.ToPrimaryQueryParams(context.ResourceID, context); primaryQuerySQL != "" {
if !context.GetDB().First(result, append([]interface{}{primaryQuerySQL}, primaryParams...)...).RecordNotFound() {
return context.GetDB().Delete(result).Error
}
}
return gorm.ErrRecordNotFound
}
return roles.ErrPermissionDenied
}
resource/meta_value.go
MetaValue数组
type MetaValues struct {
Values []*MetaValue
}
通过名字获取Meta值
func (mvs MetaValues) Get(name string) *MetaValue {
for _, mv := range mvs.Values {
if mv.Name == name {
return mv
}
}
return nil
}
MetaValue是用于保存信息的结构,将HTTP表单、JSON、CSV文件等的输入转换为meta值 它包括字段名、字段值及其配置的Meta,如果是嵌套资源,将在MetaValues中包括嵌套的Meta
type MetaValue struct {
Name string
Value interface{}
Index int
Meta Metaor
MetaValues *MetaValues
}
func decodeMetaValuesToField(res Resourcer, field reflect.Value, metaValue *MetaValue, context *qor.Context) {
if field.Kind() == reflect.Struct {
value := reflect.New(field.Type())
associationProcessor := DecodeToResource(res, value.Interface(), metaValue.MetaValues, context)
associationProcessor.Start()
if !associationProcessor.SkipLeft {
field.Set(value.Elem())
}
} else if field.Kind() == reflect.Slice {
if metaValue.Index == 0 {
field.Set(reflect.Zero(field.Type()))
}
var fieldType = field.Type().Elem()
var isPtr bool
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
isPtr = true
}
value := reflect.New(fieldType)
associationProcessor := DecodeToResource(res, value.Interface(), metaValue.MetaValues, context)
associationProcessor.Start()
if !associationProcessor.SkipLeft {
if !reflect.DeepEqual(reflect.Zero(fieldType).Interface(), value.Elem().Interface()) {
if isPtr {
field.Set(reflect.Append(field, value))
} else {
field.Set(reflect.Append(field, value.Elem()))
}
}
}
}
}
resource/meta.go
此文件涉及旧版GORM
分离ID和version_name等复合主键 const CompositePrimaryKeySeparator = “^|^”
表示复合主键的字符串 const CompositePrimaryKeyFieldName = “CompositePrimaryKeyField”
要嵌入到需要组合主键的结构中,请选择多个 ??
type CompositePrimaryKeyField struct {
CompositePrimaryKey string `gorm:"-"`
}
临时存储id和版本组合的容器
type CompositePrimaryKeyStruct struct {
ID uint `json:"id"`
VersionName string `json:"version_name"`
}
以特定格式生成复合主键
func GenCompositePrimaryKey(id interface{}, versionName string) string {
return fmt.Sprintf("%d%s%s", id, CompositePrimaryKeySeparator, versionName)
}
元接口
type Metaor interface {
GetName() string
GetFieldName() string
GetSetter() func(resource interface{}, metaValue *MetaValue, context *qor.Context)
GetFormattedValuer() func(interface{}, *qor.Context) interface{}
GetValuer() func(interface{}, *qor.Context) interface{}
GetResource() Resourcer
GetMetas() []Metaor
SetPermission(*roles.Permission)
HasPermission(roles.PermissionMode, *qor.Context) bool
}
如果结构的字段类型实现了此接口,则在初始化元时将调用它
type ConfigureMetaBeforeInitializeInterface interface {
ConfigureQorMetaBeforeInitialize(Metaor)
}
如果结构的字段类型实现了此接口,则在配置后将调用它
type ConfigureMetaInterface interface {
ConfigureQorMeta(Metaor)
}
元配置接口
type MetaConfigInterface interface {
ConfigureMetaInterface
}
基本元配置结构
type MetaConfig struct {
}
实现元配置接口
func (MetaConfig) ConfigureQorMeta(Metaor) {
}
元结构定义
type Meta struct {
Name string
FieldName string
FieldStruct *gorm.StructField
Setter func(resource interface{}, metaValue *MetaValue, context *qor.Context)
Valuer func(interface{}, *qor.Context) interface{}
FormattedValuer func(interface{}, *qor.Context) interface{}
Config MetaConfigInterface
BaseResource Resourcer
Resource Resourcer
Permission *roles.Permission
}
从meta获取基本资源
func (meta Meta) GetBaseResource() Resourcer {
return meta.BaseResource
}
得到meta的名字
func (meta Meta) GetName() string {
return meta.Name
}
获取meta的字段名
func (meta Meta) GetFieldName() string {
return meta.FieldName
}
设置meta的字段名
func (meta *Meta) SetFieldName(name string) {
meta.FieldName = name
}
Meta中获取setter
func (meta Meta) GetSetter() func(resource interface{}, metaValue *MetaValue, context *qor.Context) {
return meta.Setter
}
设置Meta的setter
func (meta *Meta) SetSetter(fc func(resource interface{}, metaValue *MetaValue, context *qor.Context)) {
meta.Setter = fc
}
获取Meta的valuer
func (meta Meta) GetValuer() func(interface{}, *qor.Context) interface{} {
return meta.Valuer
}
设置Meta的valuer
func (meta *Meta) SetValuer(fc func(interface{}, *qor.Context) interface{}) {
meta.Valuer = fc
}
从meta获取格式化的valuer
func (meta *Meta) GetFormattedValuer() func(interface{}, *qor.Context) interface{} {
if meta.FormattedValuer != nil {
return meta.FormattedValuer
}
return meta.Valuer
}
设置meta的格式化valuer
func (meta *Meta) SetFormattedValuer(fc func(interface{}, *qor.Context) interface{}) {
meta.FormattedValuer = fc
}
检查是否有权限
func (meta Meta) HasPermission(mode roles.PermissionMode, context *qor.Context) bool {
if meta.Permission == nil {
return true
}
var roles = []interface{}{}
for _, role := range context.Roles {
roles = append(roles, role)
}
return meta.Permission.HasPermission(mode, roles...)
}
设置Meta的权限
func (meta *Meta) SetPermission(permission *roles.Permission) {
meta.Permission = permission
}
初始化前运行,用于填写一些基本的必要信息
func (meta *Meta) PreInitialize() error {
if meta.Name == "" {
utils.ExitWithMsg("Meta should have name: %v", reflect.TypeOf(meta))
} else if meta.FieldName == "" {
meta.FieldName = meta.Name
}
// parseNestedField used to handle case like Profile.Name
var parseNestedField = func(value reflect.Value, name string) (reflect.Value, string) {
fields := strings.Split(name, ".")
value = reflect.Indirect(value)
for _, field := range fields[:len(fields)-1] {
value = value.FieldByName(field)
}
return value, fields[len(fields)-1]
}
var getField = func(fields []*gorm.StructField, name string) *gorm.StructField {
for _, field := range fields {
if field.Name == name || field.DBName == name {
return field
}
}
return nil
}
var nestedField = strings.Contains(meta.FieldName, ".")
var scope = &gorm.Scope{Value: meta.BaseResource.GetResource().Value}
if nestedField {
subModel, name := parseNestedField(reflect.ValueOf(meta.BaseResource.GetResource().Value), meta.FieldName)
meta.FieldStruct = getField(scope.New(subModel.Interface()).GetStructFields(), name)
} else {
meta.FieldStruct = getField(scope.GetStructFields(), meta.FieldName)
}
return nil
}
初始化meta,将设置valuer,setter
func (meta *Meta) Initialize() error {
// Set Valuer for Meta
if meta.Valuer == nil {
setupValuer(meta, meta.FieldName, meta.GetBaseResource().NewStruct())
}
if meta.Valuer == nil {
utils.ExitWithMsg("Meta %v is not supported for resource %v, no `Valuer` configured for it", meta.FieldName, reflect.TypeOf(meta.BaseResource.GetResource().Value))
}
// Set Setter for Meta
if meta.Setter == nil {
setupSetter(meta, meta.FieldName, meta.GetBaseResource().NewStruct())
}
return nil
}
如果关联集成了CompositePrimaryKey,则会按照我们的常规格式为其生成值。PrimaryKeyOf函数将从CompositePrimaryKey而不是ID返回值,以便前端可以找到正确的版本
func setCompositePrimaryKey(f *gorm.Field) {
for i := 0; i < f.Field.Len(); i++ {
associatedRecord := reflect.Indirect(f.Field.Index(i))
if v := associatedRecord.FieldByName(CompositePrimaryKeyFieldName); v.IsValid() {
id := associatedRecord.FieldByName("ID").Uint()
versionName := associatedRecord.FieldByName("VersionName").String()
associatedRecord.FieldByName("CompositePrimaryKey").SetString(fmt.Sprintf("%d%s%s", id, CompositePrimaryKeySeparator, versionName))
}
}
}
func setupValuer(meta *Meta, fieldName string, record interface{}) {
nestedField := strings.Contains(fieldName, ".")
// Setup nested fields
if nestedField {
fieldNames := strings.Split(fieldName, ".")
setupValuer(meta, strings.Join(fieldNames[1:], "."), getNestedModel(record, strings.Join(fieldNames[0:2], "."), nil))
oldValuer := meta.Valuer
meta.Valuer = func(record interface{}, context *qor.Context) interface{} {
return oldValuer(getNestedModel(record, strings.Join(fieldNames[0:2], "."), context), context)
}
return
}
if meta.FieldStruct != nil {
meta.Valuer = func(value interface{}, context *qor.Context) interface{} {
// get scope of current record. like Collection, then iterate its fields
scope := context.GetDB().NewScope(value)
if f, ok := scope.FieldByName(fieldName); ok {
if relationship := f.Relationship; relationship != nil && f.Field.CanAddr() && !scope.PrimaryKeyZero() {
// Iterate each field see if it is an relationship field like
// Factories []factory.Factory
// If so, set the CompositePrimaryKey value for PrimaryKeyOf to read
if (relationship.Kind == "has_many" || relationship.Kind == "many_to_many") && f.Field.Len() == 0 {
// Retrieve the associated records from db
context.GetDB().Set("publish:version:mode", "multiple").Model(value).Related(f.Field.Addr().Interface(), fieldName)
setCompositePrimaryKey(f)
} else if (relationship.Kind == "has_one" || relationship.Kind == "belongs_to") && context.GetDB().NewScope(f.Field.Interface()).PrimaryKeyZero() {
if f.Field.Kind() == reflect.Ptr && f.Field.IsNil() {
f.Field.Set(reflect.New(f.Field.Type().Elem()))
}
context.GetDB().Set("publish:version:mode", "multiple").Model(value).Related(f.Field.Addr().Interface(), fieldName)
}
}
return f.Field.Interface()
}
return ""
}
}
}
用于在创建新版本时切换到记录的新版本 给定记录必须定义函数“AssignVersionName”,并带有指针接收器,以便在新版本上创建关联。否则,该操作将被省略。 // e.g. the user is creating a new version based on version “2021-3-3-v1”. which would be “2021-3-3-v2”. // the associations added during the creation should be associated with “2021-3-3-v2” rather than “2021-3-3-v1”
func switchRecordToNewVersionIfNeeded(context *qor.Context, record interface{}) interface{} {
if context.Request == nil {
return record
}
currentVersionName := context.Request.Form.Get("QorResource.VersionName")
recordValue := reflect.ValueOf(record)
if recordValue.Kind() == reflect.Ptr {
recordValue = recordValue.Elem()
}
// Handle situation when the primary key is a uint64 not general uint
var id uint64
idUint, ok := recordValue.FieldByName("ID").Interface().(uint)
if !ok {
id64, ok := recordValue.FieldByName("ID").Interface().(uint64)
if !ok {
panic("ID filed must be uint or uint64")
}
id = id64
} else {
id = uint64(idUint)
}
// if currentVersionName is blank, we consider it is creating a new version
if id != 0 && currentVersionName == "" {
arguments := []reflect.Value{reflect.ValueOf(context.GetDB())}
// Handle the situation when record is NOT a pointer
if reflect.ValueOf(record).Kind() != reflect.Ptr {
// We create a new pointer to be able to invoke the AssignVersionName method on Pointer receiver
recordPtr := reflect.New(reflect.TypeOf(record))
recordPtr.Elem().Set(reflect.ValueOf(record))
fn := recordPtr.MethodByName("AssignVersionName")
if !fn.IsValid() {
log.Printf("Struct %v must has function 'AssignVersionName' defined, with *Pointer* receiver to create associations on new version", reflect.TypeOf(record).Name())
return record
}
fn.Call(arguments)
// Since it is a new pointer, we have to return the new record
return recordPtr.Elem().Interface()
}
// When the record is a pointer
fn := reflect.ValueOf(record).MethodByName("AssignVersionName")
if !fn.IsValid() {
log.Printf("Struct %v must has function 'AssignVersionName' defined, with *Pointer* receiver to create associations on new version", reflect.TypeOf(record).Name())
return record
}
// AssignVersionName set the record's version name as the new version, so when execute the SQL, we can find correct object to apply the association
fn.Call(arguments)
return record
}
return record
}
func HandleBelongsTo(context *qor.Context, record reflect.Value, field reflect.Value, relationship *gorm.Relationship, primaryKeys []string) {
// Read value from foreign key field. e.g. TagID => 1
oldPrimaryKeys := utils.ToArray(record.FieldByName(relationship.ForeignFieldNames[0]).Interface())
// if not changed, return immediately
if fmt.Sprint(primaryKeys) == fmt.Sprint(oldPrimaryKeys) {
return
}
foreignKeyField := record.FieldByName(relationship.ForeignFieldNames[0])
if len(primaryKeys) == 0 {
// if foreign key removed
foreignKeyField.Set(reflect.Zero(foreignKeyField.Type()))
} else {
// if foreign key changed. We need to make sure the field is a blank object
// Suppose this is a Collection belongs to Tag association
// non-blank field will perform a query like `SELECT * FROM "tags" WHERE "tags"."deleted_at" IS NULL AND "tags"."id" = 1 AND (("tags"."id" IN ('2')))`
// Usually this won't happen, cause the Tag field of Collection will be blank by default. it is a double assurance.
field.FieldByName("ID").SetUint(0)
context.GetDB().Set("publish:version:mode", "multiple").Where(primaryKeys).Find(field.Addr().Interface())
}
}
func HandleVersioningBelongsTo(context *qor.Context, record reflect.Value, field reflect.Value, relationship *gorm.Relationship, primaryKeys []string, fieldHasVersion bool) {
foreignKeyName := relationship.ForeignFieldNames[0]
// Construct version name foreign key. e.g. ManagerID -> ManagerVersionName
foreignVersionName := strings.Replace(foreignKeyName, "ID", "VersionName", -1)
foreignKeyField := record.FieldByName(foreignKeyName)
foreignVersionField := record.FieldByName(foreignVersionName)
oldPrimaryKeys := utils.ToArray(foreignKeyField.Interface())
// If field struct has version and it defined XXVersionName foreignKey field
// then construct ID+VersionName and compare with composite primarykey
if fieldHasVersion && len(oldPrimaryKeys) != 0 && foreignVersionField.IsValid() {
oldPrimaryKeys[0] = GenCompositePrimaryKey(oldPrimaryKeys[0], foreignVersionField.String())
}
// if not changed
if fmt.Sprint(primaryKeys) == fmt.Sprint(oldPrimaryKeys) {
return
}
// foreignkey removed
if len(primaryKeys) == 0 {
foreignKeyField.Set(reflect.Zero(foreignKeyField.Type()))
// if field has version, we have to set both the id and version_name to zero value.
if fieldHasVersion {
foreignKeyField.Set(reflect.Zero(foreignKeyField.Type()))
foreignVersionField.Set(reflect.Zero(foreignVersionField.Type()))
}
// foreignkey updated
} else {
// if foreign key changed. We need to make sure the field is a blank object
// Suppose this is a Collection belongs to Tag association
// non-blank field will perform a query like `SELECT * FROM "tags" WHERE "tags"."deleted_at" IS NULL AND "tags"."id" = 1 AND (("tags"."id" IN ('2')))`
// Usually this won't happen, cause the Tag field of Collection will be blank by default. it is a double assurance.
field.FieldByName("ID").SetUint(0)
compositePKeys := strings.Split(primaryKeys[0], CompositePrimaryKeySeparator)
// If primaryKeys doesn't include version name, process it as an ID
if len(compositePKeys) == 1 {
context.GetDB().Set("publish:version:mode", "multiple").Where(primaryKeys).Find(field.Addr().Interface())
} else {
context.GetDB().Set("publish:version:mode", "multiple").Where("id = ? AND version_name = ?", compositePKeys[0], compositePKeys[1]).Find(field.Addr().Interface())
}
}
}
func CollectPrimaryKeys(metaValueForCompositePrimaryKeys []string) (compositePKeys []CompositePrimaryKeyStruct, compositePKeyConvertErr error) {
// To convert []string{"1^|^2020-09-14-v1", "2^|^2020-09-14-v3"} to []compositePrimaryKey
for _, rawCpk := range metaValueForCompositePrimaryKeys {
// Skip blank string when it is not the only element
if len(rawCpk) == 0 && len(metaValueForCompositePrimaryKeys) > 1 {
continue
}
pks := strings.Split(rawCpk, CompositePrimaryKeySeparator)
if len(pks) != 2 {
compositePKeyConvertErr = errors.New("metaValue is not for composite primary key")
break
}
id, convErr := strconv.ParseUint(pks[0], 10, 32)
if convErr != nil {
compositePKeyConvertErr = fmt.Errorf("composite primary key has incorrect id %s", pks[0])
break
}
cpk := CompositePrimaryKeyStruct{
ID: uint(id),
VersionName: pks[1],
}
compositePKeys = append(compositePKeys, cpk)
}
return
}
func HandleManyToMany(context *qor.Context, scope *gorm.Scope, meta *Meta, record interface{}, metaValue *MetaValue, field reflect.Value, fieldHasVersion bool) {
metaValueForCompositePrimaryKeys, ok := metaValue.Value.([]string)
compositePKeys := []CompositePrimaryKeyStruct{}
var compositePKeyConvertErr error
if ok {
compositePKeys, compositePKeyConvertErr = CollectPrimaryKeys(metaValueForCompositePrimaryKeys)
}
// If the field is a struct with version and metaValue is present and we can collect id + version_name combination
// It means we can make the query by specific condition
if fieldHasVersion && metaValue.Value != nil && compositePKeyConvertErr == nil && len(compositePKeys) > 0 {
HandleVersionedManyToMany(context, field, compositePKeys)
} else {
HandleNormalManyToMany(context, field, metaValue, fieldHasVersion, compositePKeyConvertErr)
}
if !scope.PrimaryKeyZero() {
context.GetDB().Model(record).Association(meta.FieldName).Replace(field.Interface())
field.Set(reflect.Zero(field.Type()))
}
}
// HandleNormalManyToMany not only handle normal many_to_many relationship, it also handled the situation that user set the association to blank
func HandleNormalManyToMany(context *qor.Context, field reflect.Value, metaValue *MetaValue, fieldHasVersion bool, compositePKeyConvertErr error) {
if fieldHasVersion && metaValue.Value != nil && compositePKeyConvertErr != nil {
fmt.Println("given meta value contains no version name, this might cause the association is incorrect")
}
primaryKeys := utils.ToArray(metaValue.Value)
if metaValue.Value == nil {
primaryKeys = []string{}
}
// set current field value to blank. This line responsible for set field to blank value when metaValue is nil
// which means user removed all associations
field.Set(reflect.Zero(field.Type()))
if len(primaryKeys) > 0 {
// replace it with new value
context.GetDB().Set("publish:version:mode", "multiple").Where(primaryKeys).Find(field.Addr().Interface())
}
}
// HandleVersionedManyToMany handle id+version_name composite primary key, query and set the correct result to the "Many" field
// e.g. Collection.Products
// This doesn't handle compositePKeys is blank logic, if it is, this function should not be invoked
func HandleVersionedManyToMany(context *qor.Context, field reflect.Value, compositePKeys []CompositePrimaryKeyStruct) {
// set current field value to blank
field.Set(reflect.Zero(field.Type()))
// eliminate potential version_name condition on the main object, we don't need it when querying associated records
// it usually added by qor/publish2.
db := context.GetDB().Set("publish:version:mode", "multiple")
for i, compositePKey := range compositePKeys {
if i == 0 {
db = db.Where("id = ? AND version_name = ?", compositePKey.ID, compositePKey.VersionName)
} else {
db = db.Or("id = ? AND version_name = ?", compositePKey.ID, compositePKey.VersionName)
}
}
db.Find(field.Addr().Interface())
}
func setupSetter(meta *Meta, fieldName string, record interface{}) {
nestedField := strings.Contains(fieldName, ".")
// Setup nested fields
if nestedField {
fieldNames := strings.Split(fieldName, ".")
setupSetter(meta, strings.Join(fieldNames[1:], "."), getNestedModel(record, strings.Join(fieldNames[0:2], "."), nil))
oldSetter := meta.Setter
meta.Setter = func(record interface{}, metaValue *MetaValue, context *qor.Context) {
oldSetter(getNestedModel(record, strings.Join(fieldNames[0:2], "."), context), metaValue, context)
}
return
}
commonSetter := func(setter func(field reflect.Value, metaValue *MetaValue, context *qor.Context, record interface{})) func(record interface{}, metaValue *MetaValue, context *qor.Context) {
return func(record interface{}, metaValue *MetaValue, context *qor.Context) {
if metaValue == nil {
return
}
defer func() {
if r := recover(); r != nil {
fmt.Println(r)
debug.PrintStack()
context.AddError(validations.NewError(record, meta.Name, fmt.Sprintf("Failed to set Meta %v's value with %v, got %v", meta.Name, metaValue.Value, r)))
}
}()
field := utils.Indirect(reflect.ValueOf(record)).FieldByName(fieldName)
if field.Kind() == reflect.Ptr {
if field.IsNil() && utils.ToString(metaValue.Value) != "" {
field.Set(utils.NewValue(field.Type()).Elem())
}
if utils.ToString(metaValue.Value) == "" {
field.Set(reflect.Zero(field.Type()))
return
}
for field.Kind() == reflect.Ptr {
field = field.Elem()
}
}
if field.IsValid() && field.CanAddr() {
setter(field, metaValue, context, record)
}
}
}
// Setup belongs_to / many_to_many Setter
if meta.FieldStruct != nil {
if relationship := meta.FieldStruct.Relationship; relationship != nil {
if relationship.Kind == "belongs_to" || relationship.Kind == "many_to_many" {
meta.Setter = commonSetter(func(field reflect.Value, metaValue *MetaValue, context *qor.Context, record interface{}) {
var (
scope = context.GetDB().NewScope(record)
recordAsValue = reflect.Indirect(reflect.ValueOf(record))
)
switchRecordToNewVersionIfNeeded(context, record)
// If the field struct has version
fieldHasVersion := fieldIsStructAndHasVersion(field)
if relationship.Kind == "belongs_to" {
primaryKeys := utils.ToArray(metaValue.Value)
if metaValue.Value == nil {
primaryKeys = []string{}
}
// For normal belongs_to association
if len(relationship.ForeignFieldNames) == 1 {
HandleBelongsTo(context, recordAsValue, field, relationship, primaryKeys)
}
// For versioning association
if len(relationship.ForeignFieldNames) == 2 {
HandleVersioningBelongsTo(context, recordAsValue, field, relationship, primaryKeys, fieldHasVersion)
}
}
if relationship.Kind == "many_to_many" {
// The reason why we use `record` as an interface{} here rather than `recordAsValue` is
// we need make query by record, it must be a pointer, but belongs_to make query based on field, no need to be pointer.
HandleManyToMany(context, scope, meta, record, metaValue, field, fieldHasVersion)
}
})
return
}
}
}
field := reflect.Indirect(reflect.ValueOf(record)).FieldByName(fieldName)
for field.Kind() == reflect.Ptr {
if field.IsNil() {
field.Set(utils.NewValue(field.Type().Elem()))
}
field = field.Elem()
}
if !field.IsValid() {
return
}
switch field.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
meta.Setter = commonSetter(func(field reflect.Value, metaValue *MetaValue, context *qor.Context, record interface{}) {
field.SetInt(utils.ToInt(metaValue.Value))
})
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
meta.Setter = commonSetter(func(field reflect.Value, metaValue *MetaValue, context *qor.Context, record interface{}) {
field.SetUint(utils.ToUint(metaValue.Value))
})
case reflect.Float32, reflect.Float64:
meta.Setter = commonSetter(func(field reflect.Value, metaValue *MetaValue, context *qor.Context, record interface{}) {
field.SetFloat(utils.ToFloat(metaValue.Value))
})
case reflect.Bool:
meta.Setter = commonSetter(func(field reflect.Value, metaValue *MetaValue, context *qor.Context, record interface{}) {
if utils.ToString(metaValue.Value) == "true" {
field.SetBool(true)
} else {
field.SetBool(false)
}
})
default:
if _, ok := field.Addr().Interface().(sql.Scanner); ok {
meta.Setter = commonSetter(func(field reflect.Value, metaValue *MetaValue, context *qor.Context, record interface{}) {
if scanner, ok := field.Addr().Interface().(sql.Scanner); ok {
if metaValue.Value == nil && len(metaValue.MetaValues.Values) > 0 {
decodeMetaValuesToField(meta.Resource, field, metaValue, context)
return
}
if scanner.Scan(metaValue.Value) != nil {
if err := scanner.Scan(utils.ToString(metaValue.Value)); err != nil {
context.AddError(err)
return
}
}
}
})
} else if reflect.TypeOf("").ConvertibleTo(field.Type()) {
meta.Setter = commonSetter(func(field reflect.Value, metaValue *MetaValue, context *qor.Context, record interface{}) {
field.Set(reflect.ValueOf(utils.ToString(metaValue.Value)).Convert(field.Type()))
})
} else if reflect.TypeOf([]string{}).ConvertibleTo(field.Type()) {
meta.Setter = commonSetter(func(field reflect.Value, metaValue *MetaValue, context *qor.Context, record interface{}) {
field.Set(reflect.ValueOf(utils.ToArray(metaValue.Value)).Convert(field.Type()))
})
} else if _, ok := field.Addr().Interface().(*time.Time); ok {
meta.Setter = commonSetter(func(field reflect.Value, metaValue *MetaValue, context *qor.Context, record interface{}) {
if str := utils.ToString(metaValue.Value); str != "" {
if newTime, err := utils.ParseTime(str, context); err == nil {
field.Set(reflect.ValueOf(newTime))
}
} else {
field.Set(reflect.Zero(field.Type()))
}
})
}
}
}
func getNestedModel(value interface{}, fieldName string, context *qor.Context) interface{} {
model := reflect.Indirect(reflect.ValueOf(value))
fields := strings.Split(fieldName, ".")
for _, field := range fields[:len(fields)-1] {
if model.CanAddr() {
submodel := model.FieldByName(field)
if context != nil && context.GetDB() != nil && context.GetDB().NewRecord(submodel.Interface()) && !context.GetDB().NewRecord(model.Addr().Interface()) {
if submodel.CanAddr() {
context.GetDB().Model(model.Addr().Interface()).Association(field).Find(submodel.Addr().Interface())
model = submodel
} else {
break
}
} else {
model = submodel
}
}
}
if model.CanAddr() {
return model.Addr().Interface()
}
return nil
}
// fieldStructHasVersion determine if the given field is a struct
// if so, detect if it has publish2.Version integrated
func fieldIsStructAndHasVersion(field reflect.Value) bool {
// If the field struct has version
if field.Type().Kind() == reflect.Slice || field.Type().Kind() == reflect.Struct {
underlyingType := field.Type()
// If the field is a slice of struct, we retrive one element(struct) as a sample to determine whether it has version
// e.g. []User -> User
if field.Type().Kind() == reflect.Slice {
underlyingType = underlyingType.Elem()
if underlyingType.Kind() == reflect.Ptr {
underlyingType = underlyingType.Elem()
}
}
for i := 0; i < underlyingType.NumField(); i++ {
if underlyingType.Field(i).Name == "Version" && underlyingType.Field(i).Type.String() == "publish2.Version" {
return true
}
}
}
return false
}
resource/processor.go
此文件涉及旧版GORM
跳过左处理器错误,如果在回调之前在验证中返回此错误,则qor将停止后续处理器的进程 ??
var ErrProcessorSkipLeft = errors.New("resource: skip left")
type processor struct {
Result interface{}
Resource Resourcer
Context *qor.Context
MetaValues *MetaValues
SkipLeft bool
}
将meta值解码为资源结果
func DecodeToResource(res Resourcer, result interface{}, metaValues *MetaValues, context *qor.Context) *processor {
return &processor{Resource: res, Result: result, Context: context, MetaValues: metaValues}
}
func (processor *processor) checkSkipLeft(errs ...error) bool {
if processor.SkipLeft {
return true
}
for _, err := range errs {
if err == ErrProcessorSkipLeft {
processor.SkipLeft = true
break
}
}
return processor.SkipLeft
}
初始化处理器
func (processor *processor) Initialize() error {
err := processor.Resource.CallFindOne(processor.Result, processor.MetaValues, processor.Context)
processor.checkSkipLeft(err)
return err
}
运行验证程序
func (processor *processor) Validate() error {
var errs qor.Errors
if processor.checkSkipLeft() {
return nil
}
for _, v := range processor.Resource.GetResource().Validators {
if errs.AddError(v.Handler(processor.Result, processor.MetaValues, processor.Context)); !errs.HasError() {
if processor.checkSkipLeft(errs.GetErrors()...) {
break
}
}
}
return errs
}
func (processor *processor) decode() (errs []error) {
if processor.checkSkipLeft() || processor.MetaValues == nil {
return
}
if destroy := processor.MetaValues.Get("_destroy"); destroy != nil {
return
}
newRecord := true
scope := &gorm.Scope{Value: processor.Result}
if primaryField := scope.PrimaryField(); primaryField != nil {
if !primaryField.IsBlank {
newRecord = false
} else {
for _, metaValue := range processor.MetaValues.Values {
if metaValue.Meta != nil && metaValue.Meta.GetFieldName() == primaryField.Name {
if v := utils.ToString(metaValue.Value); v != "" && v != "0" {
newRecord = false
}
}
}
}
}
for _, metaValue := range processor.MetaValues.Values {
meta := metaValue.Meta
if meta == nil {
continue
}
if newRecord && !meta.HasPermission(roles.Create, processor.Context) {
continue
} else if !newRecord && !meta.HasPermission(roles.Update, processor.Context) {
continue
}
if setter := meta.GetSetter(); setter != nil {
setter(processor.Result, metaValue, processor.Context)
}
if metaValue.MetaValues != nil && len(metaValue.MetaValues.Values) > 0 {
if res := metaValue.Meta.GetResource(); res != nil && !reflect.ValueOf(res).IsNil() {
field := reflect.Indirect(reflect.ValueOf(processor.Result)).FieldByName(meta.GetFieldName())
// Only decode nested meta value into struct if no Setter defined
if meta.GetSetter() == nil || reflect.Indirect(field).Type() == utils.ModelType(res.NewStruct()) {
if _, ok := field.Addr().Interface().(sql.Scanner); !ok {
decodeMetaValuesToField(res, field, metaValue, processor.Context)
}
}
}
}
}
return
}
启动处理器
func (processor *processor) Start() error {
var errs qor.Errors
processor.Initialize()
if errs.AddError(processor.Validate()); !errs.HasError() {
errs.AddError(processor.Commit())
}
if errs.HasError() {
return errs
}
return nil
}
将数据提交到结果中
func (processor *processor) Commit() error {
var errs qor.Errors
errs.AddError(processor.decode()...)
if processor.checkSkipLeft(errs.GetErrors()...) {
return nil
}
for _, p := range processor.Resource.GetResource().Processors {
if err := p.Handler(processor.Result, processor.MetaValues, processor.Context); err != nil {
if processor.checkSkipLeft(err) {
break
}
errs.AddError(err)
}
}
return errs
}
resouce/resource.go
此文件存在旧版本GORM的调用
Resourcer接口
type Resourcer interface {
GetResource() *Resource
GetMetas([]string) []Metaor
CallFindMany(interface{}, *qor.Context) error
CallFindOne(interface{}, *MetaValues, *qor.Context) error
CallSave(interface{}, *qor.Context) error
CallDelete(interface{}, *qor.Context) error
NewSlice() interface{}
NewStruct() interface{}
}
如果一个结构实现了这个接口,那么在使用该结构创建资源时,将首先调用它
type ConfigureResourceBeforeInitializeInterface interface {
ConfigureQorResourceBeforeInitialize(Resourcer)
}
如果一个结构实现了这个接口,在用户配置之后会调用它
type ConfigureResourceInterface interface {
ConfigureQorResource(Resourcer)
}
包含qor资源基本定义的结构
type Resource struct {
Name string
Value interface{}
PrimaryFields []*gorm.StructField
FindManyHandler func(interface{}, *qor.Context) error
FindOneHandler func(interface{}, *MetaValues, *qor.Context) error
SaveHandler func(interface{}, *qor.Context) error
DeleteHandler func(interface{}, *qor.Context) error
Permission *roles.Permission
Validators []*Validator
Processors []*Processor
primaryField *gorm.Field
}
初始化qor资源
func New(value interface{}) *Resource {
var (
name = utils.HumanizeString(utils.ModelType(value).Name())
res = &Resource{Value: value, Name: name}
)
res.FindOneHandler = res.findOneHandler
res.FindManyHandler = res.findManyHandler
res.SaveHandler = res.saveHandler
res.DeleteHandler = res.deleteHandler
res.SetPrimaryFields()
return res
}
返回自身以匹配接口“Resourcer”`
func (res *Resource) GetResource() *Resource {
return res
}
设置主字段
func (res *Resource) SetPrimaryFields(fields ...string) error {
scope := gorm.Scope{Value: res.Value}
res.PrimaryFields = nil
if len(fields) > 0 {
for _, fieldName := range fields {
if field, ok := scope.FieldByName(fieldName); ok {
res.PrimaryFields = append(res.PrimaryFields, field.StructField)
} else {
return fmt.Errorf("%v is not a valid field for resource %v", fieldName, res.Name)
}
}
return nil
}
if primaryField := scope.PrimaryField(); primaryField != nil {
res.PrimaryFields = []*gorm.StructField{primaryField.StructField}
return nil
}
return fmt.Errorf("no valid primary field for resource %v", res.Name)
}
验证器结构
type Validator struct {
Name string
Handler func(interface{}, *MetaValues, *qor.Context) error
}
将验证器添加到资源中,它将在创建、更新时调用,如果验证器返回任何错误,它将回滚更改
func (res *Resource) AddValidator(validator *Validator) {
for idx, v := range res.Validators {
if v.Name == validator.Name {
res.Validators[idx] = validator
return
}
}
res.Validators = append(res.Validators, validator)
}
处理器结构
type Processor struct {
Name string
Handler func(interface{}, *MetaValues, *qor.Context) error
}
将处理器添加到资源,用于在创建、更新之前处理数据,如果返回任何错误,将回滚更改
func (res *Resource) AddProcessor(processor *Processor) {
for idx, p := range res.Processors {
if p.Name == processor.Name {
res.Processors[idx] = processor
return
}
}
res.Processors = append(res.Processors, processor)
}
初始化资源的结构
func (res *Resource) NewStruct() interface{} {
if res.Value == nil {
return nil
}
return reflect.New(utils.Indirect(reflect.ValueOf(res.Value)).Type()).Interface()
}
初始化资源的结构片
func (res *Resource) NewSlice() interface{} {
if res.Value == nil {
return nil
}
sliceType := reflect.SliceOf(reflect.TypeOf(res.Value))
slice := reflect.MakeSlice(sliceType, 0, 0)
slicePtr := reflect.New(sliceType)
slicePtr.Elem().Set(slice)
return slicePtr.Interface()
}
获取定义的meta,以匹配接Resourcer
func (res *Resource) GetMetas([]string) []Metaor {
panic("not defined")
}
检查资源的权限
func (res *Resource) HasPermission(mode roles.PermissionMode, context *qor.Context) bool {
if res == nil || res.Permission == nil {
return true
}
var roles = []interface{}{}
for _, role := range context.Roles {
roles = append(roles, role)
}
return res.Permission.HasPermission(mode, roles...)
}
resouce/schema.go
func convertMapToMetaValues(values map[string]interface{}, metaors []Metaor) (*MetaValues, error) {
metaValues := &MetaValues{}
metaorMap := make(map[string]Metaor)
for _, metaor := range metaors {
metaorMap[metaor.GetName()] = metaor
}
for key, value := range values {
var metaValue *MetaValue
metaor := metaorMap[key]
var childMeta []Metaor
if metaor != nil {
childMeta = metaor.GetMetas()
}
switch result := value.(type) {
case map[string]interface{}:
if children, err := convertMapToMetaValues(result, childMeta); err == nil {
metaValue = &MetaValue{Name: key, Meta: metaor, MetaValues: children}
}
case []interface{}:
for idx, r := range result {
if mr, ok := r.(map[string]interface{}); ok {
if children, err := convertMapToMetaValues(mr, childMeta); err == nil {
metaValue := &MetaValue{Name: key, Meta: metaor, MetaValues: children, Index: idx}
metaValues.Values = append(metaValues.Values, metaValue)
}
} else {
metaValue := &MetaValue{Name: key, Value: result, Meta: metaor}
metaValues.Values = append(metaValues.Values, metaValue)
break
}
}
default:
metaValue = &MetaValue{Name: key, Value: value, Meta: metaor}
}
if metaValue != nil {
metaValues.Values = append(metaValues.Values, metaValue)
}
}
return metaValues, nil
}
将json转换为meta值
func ConvertJSONToMetaValues(reader io.Reader, metaors []Metaor) (*MetaValues, error) {
var (
err error
values = map[string]interface{}{}
decoder = json.NewDecoder(reader)
)
if err = decoder.Decode(&values); err == nil {
return convertMapToMetaValues(values, metaors)
}
return nil, err
}
var (
isCurrentLevel = regexp.MustCompile("^[^.]+$")
isNextLevel = regexp.MustCompile(`^(([^.\[\]]+)(\[\d+\])?)(?:(\.[^.]+)+)$`)
)
将表单转换为Meta值
func ConvertFormToMetaValues(request *http.Request, metaors []Metaor, prefix string) (*MetaValues, error) {
metaValues := &MetaValues{}
metaorsMap := map[string]Metaor{}
convertedNextLevel := map[string]bool{}
nestedStructIndex := map[string]int{}
for _, metaor := range metaors {
metaorsMap[metaor.GetName()] = metaor
}
newMetaValue := func(key string, value interface{}) {
if strings.HasPrefix(key, prefix) {
var metaValue *MetaValue
key = strings.TrimPrefix(key, prefix)
if matches := isCurrentLevel.FindStringSubmatch(key); len(matches) > 0 {
name := matches[0]
metaValue = &MetaValue{Name: name, Meta: metaorsMap[name], Value: value}
} else if matches := isNextLevel.FindStringSubmatch(key); len(matches) > 0 {
name := matches[1]
if _, ok := convertedNextLevel[name]; !ok {
var metaors []Metaor
convertedNextLevel[name] = true
metaor := metaorsMap[matches[2]]
if metaor != nil {
metaors = metaor.GetMetas()
}
if children, err := ConvertFormToMetaValues(request, metaors, prefix+name+"."); err == nil {
nestedName := prefix + matches[2]
if _, ok := nestedStructIndex[nestedName]; ok {
nestedStructIndex[nestedName]++
} else {
nestedStructIndex[nestedName] = 0
}
// is collection
if matches[3] != "" {
metaValue = &MetaValue{Name: matches[2], Meta: metaor, MetaValues: children, Index: nestedStructIndex[nestedName]}
} else {
// is nested and it is existing
if metaValue = metaValues.Get(matches[2]); metaValue == nil {
metaValue = &MetaValue{Name: matches[2], Meta: metaor, MetaValues: children, Index: nestedStructIndex[nestedName]}
} else {
metaValue.MetaValues = children
metaValue.Index = nestedStructIndex[nestedName]
metaValue = nil
}
}
}
}
}
if metaValue != nil {
metaValues.Values = append(metaValues.Values, metaValue)
}
}
}
var sortedFormKeys []string
for key := range request.Form {
sortedFormKeys = append(sortedFormKeys, key)
}
utils.SortFormKeys(sortedFormKeys)
for _, key := range sortedFormKeys {
newMetaValue(key, request.Form[key])
}
if request.MultipartForm != nil {
sortedFormKeys = []string{}
for key := range request.MultipartForm.File {
sortedFormKeys = append(sortedFormKeys, key)
}
utils.SortFormKeys(sortedFormKeys)
for _, key := range sortedFormKeys {
newMetaValue(key, request.MultipartForm.File[key])
}
}
return metaValues, nil
}
根据资源定义将上下文解码为结果
func Decode(context *qor.Context, result interface{}, res Resourcer) error {
var errors qor.Errors
var err error
var metaValues *MetaValues
metaors := res.GetMetas([]string{})
if strings.Contains(context.Request.Header.Get("Content-Type"), "json") {
metaValues, err = ConvertJSONToMetaValues(context.Request.Body, metaors)
context.Request.Body.Close()
} else {
metaValues, err = ConvertFormToMetaValues(context.Request, metaors, "QorResource.")
}
errors.AddError(err)
errors.AddError(DecodeToResource(res, result, metaValues, context).Start())
return errors
}