OPERATION钩子

当客户端请求OPERATION编译成的API时(对应9991端口),依次经历登录校验,授权校验,入参校验,参数注入,执行OPERATION,响应转换等步骤。

接下来,我们学习下,该流程图上的分支:OPERATION钩子。

它分为2大类:全局钩子和局部钩子。

全局钩子所有的API共用同一个,都可以修改请求头对象,即ClientRequest。全局钩子有3个,包括:网关钩子(beforeOriginRequest)、数据源前置钩子(onOriginRequest)、数据源后置钩子(onOriginResponse)。

局部钩子每个API单独启停,从是否改变请求逻辑的角度,也可以分为两类:

  • 不改变流程的前后置钩子:

    • 前置钩子:preResolve、mutatingPreResolve

    • 后置钩子:postResolve、mutatingPostResolve

  • 能改变流程:

    • 模拟钩子:mockResolve

    • 自定义钩子:customResolve

HTTP请求流程图

为了方便理解,我们采用如下OPERATION说明情况。

Weather.graphql
query MyQuery($capital: String!) {
  weather_getCityByName(name: $capital) {
    weather {
      summary {
        title
        description
      }
      temperature {
        actual
        feelsLike
      }
    }
  }
}

全局钩子

网关钩子

beforeOriginRequest 钩子又名网关钩子,会拦截所有HTTP请求,可以改写请求内容,也可以终止后续流程。

http://{serverAddress}/global/httpTransport/beforeOriginRequest

# Example:: http://localhost:9992/global/httpTransport/beforeOriginRequest

Content-Type: application/json
X-Request-Id: "83821325-9638-e1af-f27d-234624aa1824"

# JSON request
{
  "request": { # 客户端请求对象,即请求9991端口的参数
    "method": "GET", 
    "requestURI":"/operations/Weather?capital=DE",
    "headers": {
      "Content-Type": "application/json; charset=utf-8"
    },
    "body":null
  },
  "operationName": "Weather",
  "operationType": "query"
}

# JSON response
{
  "op": "Weather",
  "hook": "beforeOriginRequest",
  "response": {
    "skip": false, # 如果true,忽略该钩子的响应
    "cancel": false, # 如果true,取消请求
    "request": { # 修改后的客户端请求对象
      "method": "GET",
      "requestURI":"/operations/Weather?capital=DE",
      "headers": {
        "content-type": "application/json; charset=utf-8"
      },
      "body":null
    }
  }
}

暂未支持

数据源前置钩子

onOriginRequest 钩子又名数据源前置钩子,在请求每个数据源之前生效。可修改请求数据源时的请求头,也可以用来取消请求。常见用例是:动态授权,动态注入授权参数

http://{serverAddress}/global/httpTransport/onOriginResponse

# Example:: http://localhost:9992/global/httpTransport/onOriginResponse

Content-Type: application/json
X-Request-Id: "83850325-9638-e5af-f27d-234624aa1824"

# JSON request
{
  "request": {
    "method": "POST", # 请求钩子的方法
    "requestURI": "https://weather-api.fireboom.io/", # 数据源的请求地址,可用来区分不同数据源
    "headers": {
      "Accept": "application/json",
      "Content-Type": "application/json",
      "X-Request-Id": "83850325-9638-e5af-f27d-234624aa1824"
    },
    "body": { # 请求数据源的body
      "variables": {
        "capital": "beijing"
      },
      "query": "query($capital: String!){weather_getCityByName: getCityByName(name: $capital){weather {summary {title description} temperature {actual feelsLike}}}}"
    }
  },
  "operationName": "Weather", # OPERATION 名称
  "operationType": "query", # OPERATION类型,QUERY、MUTATION、SUBSCRIPTION
  "__wg": { # 全局参数
    "clientRequest": { # 原始客户端请求,即请求9991端口的request对象
      "method": "GET",
      "requestURI": "/operations/Weather?code=beijing",
      "headers": {
        "Accept": "application/json",
        "Content-Type": "application/json"
      }
    },
    "user": { # (可选)授权用户的信息
      "userID": "1",
      "roles": ["user"]
    }
  }
}

# JSON response
{
  "op": "Weather",
  "hook": "onOriginRequest",
  "response": {
    "skip": false, # 如果true,忽略该钩子的响应
    "cancel": false, # 如果true,取消请求
    "request": { # 修改访问数据源的请求对象
      "method": "POST",
      "requestURI": "https://weather-api.fireboom.com/",
      "headers": {
        "Accept": "application/json",
        "Content-Type": "application/json",
        "X-Request-Id": "83850325-9638-e5af-f27d-234624aa1824"
      },
      "body": {
        "variables": { "capital": "beijing" },
        "query": "query($capital: String!){weather_getCityByName: getCityByName(name: $capital){weather {summary {title description} temperature {actual feelsLike}}}}"
      }
    }
  }
}
func OnOriginRequest(hook *base.HttpTransportHookRequest, body *plugins.HttpTransportBody) (*base.ClientRequest, error) {
	fmt.Println("OnOriginRequest")
	// 修改请求数据源的请求头
	if body.Request.RequestURI == "https://api.openweathermap.org/data/2.5/weather?appid=322110335eaafeea8b31d8263910ac70&q=beijing" {
		body.Request.Headers["test1"] = "sss1"
	} else if body.Request.RequestURI == "http://localhost:58688/" {
		body.Request.Headers["test2"] = "sss2"
	} else if body.Request.RequestURI == "https://fireboom-gql.ansoncode.repl.co/graphql" {
		body.Request.Headers["test3"] = "sss3"
	} else {
		fmt.Println("未支持数据源...", body.Request.RequestURI)
	}
	fmt.Println("数据源:", body.Request.RequestURI)
	// body.Request对应上面JSON response的response.request
	return body.Request, nil
}

数据源后置钩子

onOriginRequest 钩子又名数据源后置钩子,在请求每个数据源之后生效。可修改数据源返回的数据,也可以用来取消请求。常见用例是:若数据源不支持json,可用该方法转换协议

http://{serverAddress}/global/httpTransport/onOriginResponse

# Example:: http://localhost:9992/global/httpTransport/onOriginResponse

Content-Type: application/json
X-Request-Id: "83850325-9638-e5af-f27d-234624aa1824"

# JSON request
{
  "response": {
    "statusCode": 200,
    "status": "200 OK",
    "method": "POST",
    "requestURI": "https://weather-api.fireboom.com/",
    "headers": {
      "Content-Type": "application/json; charset=utf-8"
    },
    "body": {
      "data": { # 从数据源获取的数据
            "weather_getCityByName": {
              "weather": {
                "summary": { "title": "Clear", "description": "clear sky" },
                "temperature": { "actual": 290.45, "feelsLike": 289.23 }
            }
          }
        }
    }
  },
  "operationName": "Weather",
  "operationType": "query",
  "__wg": {
    "clientRequest": {
      "method": "GET",
      "requestURI": "/operations/Weather?code=beijing",
      "headers": {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
        "Accept-Encoding": "gzip, deflate, br",
        "Accept-Language": "de-DE,de;q=0.9,en-DE;q=0.8,en;q=0.7,en-GB;q=0.6,en-US;q=0.5"
      }
    }
  }
}

# JSON response
{
  "op": "Weather",
  "hook": "onOriginResponse",
  "response": {
    "skip": false,
    "cancel": false,
    "response": {
      "statusCode": 200,
      "status": "200 OK",
      "method": "POST",
      "requestURI": "https://weather-api.fireboom.io/",
      "headers": {
        "access-control-allow-origin": "*",
        "content-type": "application/json; charset=utf-8",
        "date": "Mon, 01 May 2023 10:46:39 GMT"
      },
      "body": {
        "data": { # 钩子修改后的数据
          "weather_getCityByName": {
            "weather": {
              "summary": { "title": "Clear", "description": "clear sky" },
              "temperature": { "actual": 290.45, "feelsLike": 289.23 }
            }
          }
        }
      }
    }
  }
}
func OnOriginResponse(hook *base.HttpTransportHookRequest, body *plugins.HttpTransportBody) (*base.ClientResponse, error) {
	fmt.Println("OnOriginResponse")
	if body.Response.RequestURI == "https://api.openweathermap.org/data/2.5/weather?appid=322110335eaafeea8b31d8263910ac70&q=beijing" {
		fmt.Println("协议转换,将xml数据转成JSON")
	} else if body.Response.RequestURI == "http://localhost:58688/" {

	} else if body.Response.RequestURI == "https://fireboom-gql.ansoncode.repl.co/graphql" {

	} else {
		fmt.Println("未支持数据源...", body.Response.RequestURI)
	}
	// body.Response对应response.response
	return body.Response, nil
}

局部钩子

与全局钩子不同,每个OPERTION都有对应的局部钩子,由开关单独控制。

前置钩子

前置钩子在 "执行OPERATION"前执行,可校验参数或修改输入参数。

前置普通钩子

preResolve 钩子在参数注入后执行,能拿到请求入参,常用于入参校验。

http://{serverAddress}/operation/{operation}/preResolve

# Example:: http://localhost:9992/operation/Weather/preResolve

Content-Type: application/json
X-Request-Id: "83850325-9638-e5af-f27d-234624aa1824"

# JSON request
{
  "__wg": {
    "clientRequest": {
      "method": "GET",
      "requestURI": "/operations/Weather?code=beijing",
      "headers": {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
        "Accept-Encoding": "gzip, deflate, br",
        "Accept-Language": "de-DE,de;q=0.9,en-DE;q=0.8,en;q=0.7,en-GB;q=0.6,en-US;q=0.5",
        "Cache-Control": "max-age=0",
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"
      }
    },
    "user": {
      "userID": "1"
    }
  },
  "input": { "capital": "beijing" } # (可选)请求的输入参数
}

# JSON response
{
  "setClientRequestHeaders": { # 设置值后,可传递给后续的钩子
        "Accept": "text/html"
  },
  "op": "Weather",
  "hook": "preResolve"
}
func PreResolve(hook *base.HookRequest, body generated.WeatherBody) (res generated.WeatherBody, err error) {
	hook.Logger().Info("PreResolve")

	hook.Logger().Info("请求参数是:", body.Input.Capital)
	if body.Input.Capital != "beijing" {
		msg := fmt.Sprintf("入参必须是beijing,当前入参是:%s", body.Input.Capital)
		return body, errors.New(msg)
	}
	return body, nil
}

前置修改入参钩子

mutatingPreResolve 钩子在参数注入后执行,能拿到请求入参,也能修改请求入参。

http://{serverAddress}/operation/{operation}/mutatingPreResolve

# Example:: http://localhost:9992/operation/Weather/mutatingPreResolve

Content-Type: application/json
X-Request-Id: "83850325-9638-e5af-f27d-234624aa1824"

# JSON request
{
  "__wg": {
    "clientRequest": {
      "method": "GET",
      "requestURI": "/operations/Weather?code=beijing",
      "headers": {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
        "Accept-Encoding": "gzip, deflate, br",
        "Accept-Language": "de-DE,de;q=0.9,en-DE;q=0.8,en;q=0.7,en-GB;q=0.6,en-US;q=0.5",
        "Cache-Control": "max-age=0",
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"
      }
    },
    "user": {
      "userID": "1"
    }
  },
  "input": { "capital": "beijing" }
}

# JSON response
{
  "op": "Weather",
  "hook": "mutatingPreResolve",
  "input": { "capital": "修改为固定值" } # 用来修改入参
}
func MutatingPreResolve(hook *base.HookRequest, body generated.WeatherBody) (res generated.WeatherBody, err error) {
	hook.Logger().Info("MutatingPreResolve")
	body.Input.Capital = "修改为固定值"
	return body, nil
}

后置钩子

后置钩子在 "执行OPERATION" 后执行,可触发自定义操作或修改响应结果。

后置普通钩子

postResolve 钩子在响应转换后执行,能拿到请求入参和响应结果,常用于消息通知。

http://{serverAddress}/operation/{operation}/postResolve

# Example:: http://localhost:9992/operation/Weather/postResolve

Content-Type: application/json
X-Request-Id: "83850325-9638-e5af-f27d-234624aa1824"

# JSON request
{
  "__wg": {
    "clientRequest": {
      "method": "GET",
      "requestURI": "/operations/Weather?code=beijing",
      "headers": {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
        "Accept-Encoding": "gzip, deflate, br",
        "Accept-Language": "de-DE,de;q=0.9,en-DE;q=0.8,en;q=0.7,en-GB;q=0.6,en-US;q=0.5",
        "Cache-Control": "max-age=0",
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"
      }
    },
    "user": {
      "userID": "1"
    }
  },
  "input": { "capital": "beijing" },
   "response": {
    "data": {
      "weather_getCityByName": {
        "weather": {
          "summary": { "title": "Clear", "description": "clear sky" },
          "temperature": { "actual": 290.45, "feelsLike": 289.23 }
        }
      }
    }
  }
}

# JSON response
{
  "op": "Weather",
  "hook": "postResolve"
}
func PostResolve(hook *base.HookRequest, body generated.WeatherBody) (res generated.WeatherBody, err error) {
	hook.Logger().Info("PostResolve")
	// 可以拿到入参和响应
	hook.Logger().Info("发送一封邮件:", body.Input, body.Response.Data)
	return body, nil
}

后置修改出参钩子

mutatingPostResolve 钩子在响应转换后执行,能拿到请求入参和响应结果,也能修改响应结果。

http://{serverAddress}/operation/{operation}/mutatingPostResolve

# Example:: http://localhost:9992/operation/Weather/mutatingPostResolve

Content-Type: application/json
X-Request-Id: "83850325-9638-e5af-f27d-234624aa1824"

# JSON request
{
  "__wg": {
    "clientRequest": {
      "method": "GET",
      "requestURI": "/operations/Weather?code=beijing",
      "headers": {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
        "Accept-Encoding": "gzip, deflate, br",
        "Accept-Language": "de-DE,de;q=0.9,en-DE;q=0.8,en;q=0.7,en-GB;q=0.6,en-US;q=0.5",
        "Cache-Control": "max-age=0",
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"
      }
    },
    "user": {
      "userID": "1"
    }
  },
  "input": { "capital": "beijing" },
  "response": {
    "data": {
      "weather_getCityByName": {
        "weather": {
          "summary": { "title": "Clear", "description": "clear sky" },
          "temperature": { "actual": 290.45, "feelsLike": 289.23 }
        }
      }
    }
  }
}

# JSON response
{
  "op": "Weather",
  "hook": "mutatingPostResolve",
  "response": {
    "data": { # 修改响应的结果(响应结构可以变化!!!)
      "weather_getCityByName": {
        "weather": {
          "summary": { "title": "Clear", "description": "修改响应值" },
          "temperature": { "actual": 290.45, "feelsLike": 289.23 }
        }
      }
    }
  }
}
func MutatingPostResolve(hook *base.HookRequest, body generated.WeatherBody) (res generated.WeatherBody, err error) {
	hook.Logger().Info("MutatingPostResolve")
	if body.Input.Capital == "beijing" {
		body.Response.Data.Weather_getCityByName.Weather.Summary.Description = "修改响应值"
		return body, nil
	}
	return body, nil
}

nodejs钩子可以修改响应形状,golang钩子暂未支持。

模拟钩子

mockResolve 钩子在前置钩子后执行,可以用来模拟操作的响应。开启后,将跳过查询数据源,详情看 HTTP请求流程图

http://{serverAddress}/operation/{operation}/mockResolve

# Example:: http://localhost:9992/operation/Weather/mockResolve

Content-Type: application/json
X-Request-Id: "83850325-9638-e5af-f27d-234624aa1824"

# JSON request
{
  "__wg": {
    "clientRequest": {
      "method": "GET",
      "requestURI": "/operations/Weather?code=beijing",
      "headers": {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
        "Accept-Encoding": "gzip, deflate, br",
        "Accept-Language": "de-DE,de;q=0.9,en-DE;q=0.8,en;q=0.7,en-GB;q=0.6,en-US;q=0.5",
        "Cache-Control": "max-age=0",
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"
      }
    },
    "user": {
      "userID": "1"
    }
  },
  "input": { "capital": "beijing" }
}

# JSON response
{
  "op": "Weather",
  "hook": "mockResolve",
  "response": {
    "data": { # 模拟的数据
      "weather_getCityByName": {
        "weather": {
          "summary": { "title": "Clear", "description": "mockdata" }
        }
      }
    }
  }
}
func MockResolve(hook *base.HookRequest, body generated.WeatherBody) (res generated.WeatherBody, err error) {
	hook.Logger().Info("MockResolve")
	body.Response = &base.OperationBodyResponse[generated.WeatherResponseData]{
		Data: generated.WeatherResponseData{
			Weather_getCityByName: generated.WeatherResponseData_weather_getCityByName{
				Weather: generated.WeatherResponseData_weather_getCityByName_weather{
					Summary: generated.WeatherResponseData_weather_getCityByName_weather_summary{
						Description: "mock data",
						Title:       "Clear",
					},
				},
			},
		},
	}
	return body, nil
}

自定义钩子

customResolve 钩子在模拟钩子后执行。此钩子可用于用自定义OPERATION解析器替换默认解析器。

有两个逻辑:

  • 返回结构体:跳过执行OPERATION的逻辑,返回结构体

  • 返回NULL:继续执行OPERATION逻辑

http://{serverAddress}/operation/{operation}/customResolve

# Example:: http://localhost:9992/operation/Weather/customResolve

Content-Type: application/json
X-Request-Id: "83850325-9638-e5af-f27d-234624aa1824"

# JSON request
{
  "__wg": {
    "clientRequest": {
      "method": "GET",
      "requestURI": "/operations/Weather?code=beijing",
      "headers": {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
        "Accept-Encoding": "gzip, deflate, br",
        "Accept-Language": "de-DE,de;q=0.9,en-DE;q=0.8,en;q=0.7,en-GB;q=0.6,en-US;q=0.5",
        "Cache-Control": "max-age=0",
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"
      }
    },
    "user": {
      "userID": "1"
    }
  },
  "input": { "capital": "beijing" }
}

# JSON response
{
  "op": "Weather",
  "hook": "customResolve",
  "response": { # 若response不为空会中断后置钩子执行
    "data": {
      "weather_getCityByName": {
        "weather": {
          "summary": { "title": "Clear", "description": "custom data" }
        }
      }
    }
  }
}
func CustomResolve(hook *base.HookRequest, body generated.WeatherBody) (res generated.WeatherBody, err error) {
	hook.Logger().Info("CustomResolve")
	// 1,如果返回null,则继续接下来的逻辑
	if body.Input.Capital == "beijing" {
		hook.Logger().Info("写一些自定义逻辑")
		// 继续后面的流程
		return nil, nil
	}
	// 2,类似mock钩子
	body.Response = &base.OperationBodyResponse[generated.WeatherResponseData]{
		Data: generated.WeatherResponseData{
			Weather_getCityByName: generated.WeatherResponseData_weather_getCityByName{
				Weather: generated.WeatherResponseData_weather_getCityByName_weather{
					Summary: generated.WeatherResponseData_weather_getCityByName_weather_summary{
						Description: "custom data",
						Title:       "Clear",
					},
				},
			},
		},
	}
	return body, nil
}

最后更新于