1. 程式人生 > >sqler sql 轉rest api 原始碼解析(四)macro 的執行

sqler sql 轉rest api 原始碼解析(四)macro 的執行

macro 說明

macro 是sqler 的核心,當前的處理流程為授權處理,資料校驗,依賴執行(include),聚合處理,資料轉換
處理

授權處理

這個是通過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 注入。。。
格式:

 
exec = <<SQL
        INSERT INTO users(name, email, password, time) VALUES(:name, :email, :password, UNIX_TIMESTAMP());
        SELECT * FROM users WHERE id = LAST_INSERT_ID();
    SQL
   

程式碼:

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
}
   

執行資料轉換

我們可能需要更具實際的需要,將資料轉換為其他的格式,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