(原) QOR再学习 -- 啃源代码,思考GORM升级

原创文章,请后转载,并注明出处。

通过上次的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
}

相关文章