sqler sql 转rest api 源码解析(四)macro 的执行

时间:2024-07-27 22:36:32

macro 说明

macro 是sqler 的核心,当前的处理流程为授权处理,数据校验,依赖执行(include),聚合处理,数据转换
处理,sql 执行以及sql 参数绑定

授权处理

这个是通过golang 的js 包处理的,通过将golang 的http 请求暴露为js 的fetch 方法,放在js 引擎执行,通过
http 状态吗确认是否是执行的权限,对于授权的处理,由宏的配置指定,建议通过http hreader处理
参考格式:

    authorizer = <<JS
        (function(){
            log("use this for debugging")
            token = $input.http_authorization
            response = fetch("http://requestbin.fullcontact.com/zxpjigzx", {
                headers: {
                    "Authorization": token
                }
            })
            if ( response.statusCode != 200 ) {
                return false
            }
            return true
        })()
    JS
  • 代码
func (m *Macro) execAuthorizer(input map[string]interface{}) (bool, error) {
  authorizer := strings.TrimSpace(m.Authorizer)
  if authorizer == "" {
    return true, nil
  }
  var execError error
 // 暴露$input  对象到js 引擎
  vm := initJSVM(map[string]interface{}{"$input": input})
 //  执行js 脚本,根据返回的状态,确认请求权限
  val, err := vm.RunString(m.Authorizer)
  if err != nil {
    return false, err
  }
  if execError != nil {
    return false, execError
  }
  return val.ToBoolean(), nil
}

数据校验处理

主要是对于传递的http 数据,转为是js 的$input 对象,通过js 引擎确认返回的状态
数据校验配置:

validators {
        user_name_is_empty = "$input.user_name && $input.user_name.trim().length > 0"
        user_email_is_empty = "$input.user_email && $input.user_email.trim(' ').length > 0"
        user_password_is_not_ok = "$input.user_password && $input.user_password.trim(' ').length > 5"
}

代码:

// validate - validate the input aginst the rules
func (m *Macro) validate(input map[string]interface{}) (ret []string, err error) {
  vm := initJSVM(map[string]interface{}{"$input": input})
  for k, src := range m.Validators {
    val, err := vm.RunString(src)
    if err != nil {
      return nil, err
    }
    if !val.ToBoolean() {
      ret = append(ret, k)
    }
  }
  return ret, err
}

依赖处理(include)

获取配置文件中include 配置的数组信息,并执行宏
一般配置如下:

    include = ["_boot"]

代码:

func (m *Macro) runIncludes(input map[string]interface{}) error {
  for _, name := range m.Include {
    macro := m.manager.Get(name)
    if nil == macro {
      return fmt.Errorf("macro %s not found", name)
    }
    _, err := macro.Call(input)
    if err != nil {
      return err
    }
  }
  return nil
}

执行聚合操作

聚合主要是减少rest 端对于宏的调用,方便数据的拼接
聚合的配置如下,只需要添加依赖的宏即可

databases_tables {
    aggregate = ["databases", "tables"]
}

代码

func (m *Macro) aggregate(input map[string]interface{}) (map[string]interface{}, error) {
    ret := map[string]interface{}{}
    for _, k := range m.Aggregate {
        macro := m.manager.Get(k)
        if nil == macro {
            err := fmt.Errorf("unknown macro %s", k)
            return nil, err
        }
        out, err := macro.Call(input)
        if err != nil {
            return nil, err
        }
        ret[k] = out
    }
    return ret, nil
}

执行sql

sql 的处理是通过text/template,同时对于多条sql 需要使用;分开,而且sql 使用的是预处理的
可以防止sql 注入。。。,同时这个阶段进行了bind 数据的处理,使用buildBind 方法
格式:

exec = <<SQL
        INSERT INTO users(name, email, password, time) VALUES(:name, :email, :password, UNIX_TIMESTAMP());
        SELECT * FROM users WHERE id = LAST_INSERT_ID();
    SQL
  • execSQLQuery代码:
func (m *Macro) execSQLQuery(sqls []string, input map[string]interface{}) (interface{}, error) {
    args, err := m.buildBind(input)
    if err != nil {
        return nil, err
    }
    conn, err := sqlx.Open(*flagDBDriver, *flagDBDSN)
    if err != nil {
        return nil, err
    }
    defer conn.Close()
    for i, sql := range sqls {
        if strings.TrimSpace(sql) == "" {
            sqls = append(sqls[0:i], sqls[i+1:]...)
        }
    }
    for _, sql := range sqls[0 : len(sqls)-1] {
        sql = strings.TrimSpace(sql)
        if "" == sql {
            continue
        }
        if _, err := conn.NamedExec(sql, args); err != nil {
            return nil, err
        }
    }
    rows, err := conn.NamedQuery(sqls[len(sqls)-1], args)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    ret := []map[string]interface{}{}
    for rows.Next() {
        row, err := m.scanSQLRow(rows)
        if err != nil {
            continue
        }
        ret = append(ret, row)
    }
    return interface{}(ret), nil
}
  • buildBind 处理
    bind 配置格式:
bind {
        name = "$input.user_name"
        email = "$input.user_email"
        password = "$input.user_password"
}

代码:

func (m *Macro) buildBind(input map[string]interface{}) (map[string]interface{}, error) {
 vm := initJSVM(map[string]interface{}{"$input": input})
 ret := map[string]interface{}{}
 for k, src := range m.Bind {
  val, err := vm.RunString(src)
  if err != nil {
   return nil, err
  }
  ret[k] = val.Export()
 }
 return ret, nil
}

执行数据转换

我们可能需要更具实际的需要,将数据转换为其他的格式,sqler 使用了js 脚本进行处理,通过暴露
$result 对象到js 运行是,然后调用转换函数对于数据进行转换
配置格式:

 transformer = <<JS
        // there is a global variable called `$result`,
        // `$result` holds the result of the sql execution.
        (function(){
            newResult = []
            for ( i in $result ) {
                newResult.push($result[i].Database)
            }
            return newResult
        })()
    JS

代码:

// execTransformer - run the transformer function
func (m *Macro) execTransformer(data interface{}) (interface{}, error) {
    transformer := strings.TrimSpace(m.Transformer)
    if transformer == "" {
        return data, nil
    }
    vm := initJSVM(map[string]interface{}{"$result": data})
    v, err := vm.RunString(transformer)
    if err != nil {
        return nil, err
    }
    return v.Export(), nil
}

sqler 对于dop251/goja 的封装处理

因为dop251/goja 设计的时候不保证并发环境下的数据一致,所以每次调用都是重新
实例化,js runtime

js vm 实例化

代码如下:
js.go

// initJSVM - creates a new javascript virtual machine
func initJSVM(ctx map[string]interface{}) *goja.Runtime {
    vm := goja.New()
    for k, v := range ctx {
        vm.Set(k, v)
    }
    vm.Set("fetch", jsFetchfunc)
    vm.Set("log", log.Println)
    return vm
}

fetch 、log 方法暴露

为了方便排查问题,以及授权中集成http 请求,所以sqler暴露了一个fetch 方法(和js 的http fetch 功能类似)
方便进行http 请求的处理
代码:

// jsFetchfunc - the fetch function used inside the js vm
func jsFetchfunc(url string, options ...map[string]interface{}) (map[string]interface{}, error) {
    var option map[string]interface{}
    var method string
    var headers map[string]string
    var body interface{}
    if len(options) > 0 {
        option = options[0]
    }
    if nil != option["method"] {
        method, _ = option["method"].(string)
    }
    if nil != option["headers"] {
        hdrs, _ := option["headers"].(map[string]interface{})
        headers = make(map[string]string)
        for k, v := range hdrs {
            headers[k], _ = v.(string)
        }
    }
    if nil != option["body"] {
        body, _ = option["body"]
    }
    resp, err := resty.R().SetHeaders(headers).SetBody(body).Execute(method, url)
    if err != nil {
        return nil, err
    }
    rspHdrs := resp.Header()
    rspHdrsNormalized := map[string]string{}
    for k, v := range rspHdrs {
        rspHdrsNormalized[strings.ToLower(k)] = v[0]
    }
    return map[string]interface{}{
        "status": resp.Status(),
        "statusCode": resp.StatusCode(),
        "headers": rspHdrsNormalized,
        "body": string(resp.Body()),
    }, nil
}

说明

基本上sqler 的源码已经完了,本身代码量不大,但是设计很简洁

参考资料

https://github.com/dop251/goja
https://github.com/alash3al/sqler/blob/master/macro.go
https://github.com/alash3al/sqler/blob/master/js.go