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
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