Rhai 脚本API参考
定制路由器API
本参考文档记录了特定于Rhai定制对于GraphOS路由器和Apollo路由器核心的符号和行为。
入口钩子
您的Rhai脚本的主文件钩子连接到路由器的各个服务的请求处理生命周期。为了这样做,它定义了所需的以下入口钩子的任意组合:
fn router_service(service) {}fn supergraph_service(service) {}fn execution_service(service) {}fn subgraph_service(service, subgraph) {}
在每个钩子内部,您定义自定义逻辑以根据需要与当前活动的请求和/或响应进行交互。这通常涉及使用提供的方法的service
对象来注册服务回调,如下所示:
fn supergraph_service(service) {let request_callback = |request| {print("Supergraph service: Client request received");};let response_callback = |response| {print("Supergraph service: Client response ready to send");};service.map_request(request_callback);service.map_response(response_callback);}
记录
如果您的脚本使用Rhai的内置print()
函数记录消息,则该消息将以router的“info”级别记录到日志中:
print("logged at the info level");
要更精细地控制消息的日志级别,您可以使用以下函数
log_error("error-level log message");log_warn("warn-level log message");log_info("info-level log message");log_debug("debug-level log message");log_trace("trace-level log message");
终止客户端请求
您的Rhai脚本可以终止触发它的关联客户端请求。为此,它抛出异常。这向客户端返回一个Internal Server Error
,带有500
响应代码。
例如
fn supergraph_service(service) {// Define a closure to process our responselet f = |response| {// Something goes wrong during response processing...throw "An error occurred setting up the supergraph_service...";};// Map our response using our closureservice.map_response(f);}
如果希望对返回给客户端的HTTP状态代码有更多控制,则可以抛出一个对象映射错误,其中包含键:状态和消息。
键必须是数字,消息必须是可以转换为字符串的事物。
例如
fn supergraph_service(service) {// Define a closure to process our responselet f = |response| {// Something goes wrong during response processing...throw #{status: 400,message: "An error occurred processing the response..."};};// Map our response using our closureservice.map_response(f);}
您还可以throw
一个有效的GraphQL响应,它将被反序列化并由router处理。
例如
fn supergraph_service(service) {// Define a closure to process our requestlet f = |request| {// Something goes wrong during request processing...throw #{status: 403,body: #{errors: [#{message: `I have raised a 403`,extensions: #{code: "ACCESS_DENIED"}}]}};};// Map our request using our closureservice.map_request(f);}
Rhai在map_request
层上抛出时会表现得与ControlFlow::Break
相同,这在外部扩展部分中进行了说明。
如果提供的状态码不是有效的HTTP状态码,则将导致500
响应码。
定时执行
您的Rhai自定义可以使用全局Router.APOLLO_START
常量来计算持续时间。这类似于Unix环境中的Epoch
。
// Define a closure to process our response// Note: We can't use a closure in this example because we are referencing our global Router during function executionfn process_response(response) {let start = Router.APOLLO_START.elapsed;// Do some processing here...let duration = Router.APOLLO_START.elapsed - start;print(`response processing took: ${duration}`);// Log out any errors we may haveprint(response.body.errors);}fn supergraph_service(service) {const response_callback = Fn("process_response");service.map_response(response_callback);}
访问SDL
您的Rhai自定义可以使用全局Router.APOLLO_SDL
常量来检查supergraph。
fn supergraph_service(service) {print(`${Router.APOLLO_SDL}`);}
访问TraceId
您的Rhai自定义可以使用函数traceid()
来检索opentelemetry跟踪ID。如果不存在span,该函数将抛出异常,因此在使用此函数时始终要处理异常。
fn supergraph_service(service) {try {let id = traceid();print(`id: ${id}`);}catch(err){// log any errorslog_error(`span id error: ${err}`);}}
URL编码/解码字符串
您的Rhai自定义可以使用函数urlencode()
和urldecode()
来编码/解码字符串。encode()
不会失败,但decode()
可能会失败,因此在使用decode()
函数时始终要处理异常。
fn supergraph_service(service) {let original = "alice and bob";let encoded = urlencode(original);// encoded will be "alice%20and%20bob"try {let and_back = urldecode(encoded);// and_back will be "alice and bob"}catch(err){// log any errorslog_error(`urldecode error: ${err}`);}}
JSON编码/解码字符串
您的Rhai自定义可以使用函数json::encode()
和json::decode()
将Rhai对象转换为有效的JSON编码字符串或从有效的JSON编码字符串转换为Rhai对象。这两个函数都可能会失败,因此在使用它们时始终要处理异常。
fn router_service(service) {let original = `{"valid":"object"}`;try {let encoded = json::decode(original);// encoded is a Rhai object, with a property (or key) named valid with a String value of "object"print(`encoded.valid: ${encoded.valid}`);let and_back = json::encode(encoded);// and_back will be a string == original.if and_back != original {throw "something has gone wrong";}}catch(err){// log any errorslog_error(`json coding error: ${err}`);}}
Base64编码/解码字符串
您的Rhai自定义可以使用函数base64::encode()
和base64::decode()
来编码/解码字符串。encode()
不会失败,但decode()
可能会失败,因此在使用decode()
函数时始终要处理异常。
fn supergraph_service(service) {let original = "alice and bob";let encoded = base64::encode(original);// encoded will be "YWxpY2UgYW5kIGJvYgo="try {let and_back = base64::decode(encoded);// and_back will be "alice and bob"}catch(err){// log any errorslog_error(`base64::decode error: ${err}`);}}
ⓘ 注意
您不需要导入“base64”模块。它在router中已经导入。
不同的字母表
Base64支持多种字母表进行数据编码,具体取决于它所在环境中支持字符。路由器支持以下字母表:
STANDARD
:定义在RFC 4648中的“base64”编码。这是未指定时的默认值。STANDARD_NO_PAD
:定义在RFC 4648中,没有了填充字符(=
或==
字符)。URL_SAFE
: 指的是在 RFC 4648 中定义的 "base64url" 编码URL_SAFE_NO_PAD
: 指的是在 RFC 4648 中定义的 "base64url" 编码,不含填充字符(=
或==
字符在末尾)
要使用它们,我们可以在 encode
和 decode
方法中添加一个 参数:
fn supergraph_service(service) {let original = "alice and bob";let encoded = base64::encode(original, base64::URL_SAFE);// encoded will be "YWxpY2UgYW5kIGJvYgo="try {let and_back = base64::decode(encoded, base64::URL_SAFE);// and_back will be "alice and bob"}catch(err){// log any errorslog_error(`base64::decode error: ${err}`);}}
SHA256散列字符串
您的Rhai定制可以使用函数 sha256::digest()
使用SHA256哈希算法对字符串进行哈希。
fn supergraph_service(service){service.map_request(|request|{let sha = sha256::digest("hello world");log_info(sha);});}
ⓘ 注意
您不需要导入 "sha256" 模块。它已包含在 router 中。
具有多个值的头部
处理单值头部的简单 get/set API 对于大多数用例来说已经足够了。如果您想对一个键设置多个值,则应提供值数组来自行设置。
如果您想获取头键的多个值,则必须使用 values()
函数,而不是索引访问器。如果您使用索引访问器,它将仅返回与键关联的第一个值(作为字符串)。
查看示例以了解其实际应用。
Unix时间戳
您的Rhai定制可以使用函数 unix_now()
以秒为单位从Unix纪元以来获取当前Unix时间戳。
fn supergraph_service(service) {let now = unix_now();}
Unix时间戳(以毫秒为单位)
您的Rhai定制可以使用函数 unix_ms_now()
以毫秒为单位从Unix纪元以来获取当前Unix时间戳。
fn supergraph_service(service) {let now = unix_ms_now();}
唯一标识符(UUID)
您的Rhai定制可以使用函数 uuid_v4()
获取UUIDv4 ID。
fn supergraph_service(service) {let id = uuid_v4();}
环境变量
您的Rhai定制可以使用 env
模块访问环境变量。使用 env::get()
函数。
fn router_service(service) {try {print(`HOME: ${env::get("HOME")}`);print(`LANG: ${env::get("LANG")}`);} catch(err) {print(`exception: ${err}`);}}
ⓘ 注意
- 您不需要导入 "env" 模块。它已包含在 router 中。
get()
可能失败,因此在使用它时最好处理异常。
可用常量
router提供了对您的Rhai脚本大部分用于从上下文获取数据的常量。
Router.APOLLO_SDL // Context key to access the SDLRouter.APOLLO_START // Constant to calculate durationsRouter.APOLLO_AUTHENTICATION_JWT_CLAIMS // Context key to access authentication jwt claimsRouter.APOLLO_SUBSCRIPTION_WS_CUSTOM_CONNECTION_PARAMS // Context key to modify or access the custom connection params when using subscriptions in WebSocket to subgraphs (cf subscription docs)Router.APOLLO_ENTITY_CACHE_KEY // Context key to access the entity cache keyRouter.APOLLO_OPERATION_ID // Context key to get the value of apollo operation id (studio trace id) from the context
Request
接口
通过 map_request
注册的所有回调函数都会接收到一个 request
对象,该对象表示客户端发送的请求。此对象包含以下 字段:
request.contextrequest.idrequest.headersrequest.methodrequest.body.queryrequest.body.operation_namerequest.body.variablesrequest.body.extensionsrequest.uri.hostrequest.uri.path
ⓘ 注意
这些 字段通常是可修改的,除了 method
外,该方法始终为只读。但是,当回调服务是 subgraph_service
时,唯一可修改的 字段 是 request.context
.
仅针对 subgraph_service
回调, request
对象提供了 额外的 可修改的字段,用于与将发送给相应 子图 的请求进行交互:
request.subgraph.headersrequest.subgraph.body.queryrequest.subgraph.body.operation_namerequest.subgraph.body.variablesrequest.subgraph.body.extensionsrequest.subgraph.uri.hostrequest.subgraph.uri.path
request.context
上下文是一个通用的键/值存储,在整个客户端请求的生命周期中存在。您可以使用它来在请求的生命周期中共享信息。
键必须是字符串,但值可以是任何Rh daß 对象。
// You can interact with request.context as an indexed variablerequest.context["contextual"] = 42; // Adds value 42 to the context with key "contextual"print(`${request.context["contextual"]}`); // Writes 42 to the router log at info level// Rhai also supports extended dot notation for indexed variables, so this is equivalentrequest.context.contextual = 42;
upsert()
上下文提供了一个 upsert()
函数来解决在设置特定键的值时需要更新或插入的情况。
要使用 upsert()
,您必须定义一个回调函数,该函数接收一个键的现有值(如果有的话)并将其修改,然后在返回最终要设置的值之前进行修改。
// Get a reference to a cache-keylet my_cache_key = response.headers["cache-key"];// Define an upsert resolver callback// The `current` parameter is the current value for the specified key.// This particular callback checks whether `current` is an ObjectMap// (default is the unit value of ()). If not, assign an empty ObjectMap.// Finally, update the stored ObjectMap with our subgraph name as key// and the returned cache-key as a value.let resolver = |current| {if current == () {// No map found. Create an empty object mapcurrent = #{};}// Update our object map with a key and valuecurrent[subgraph] = my_cache_key;return current;};// Upsert our context with our resolverresponse.context.upsert("surrogate-cache-key", resolver);
request.id
此 ID 是一个字符串,在整个生命周期中唯一地标识请求/响应上下文。如果您有一个请求(或响应),您可以通过以下方式访问 ID。
print(`request id is: ${request.id}`);
request.headers
请求的标题可以作为可读/写的索引 变量 访问。键和值必须是有效的标题名称和值字符串。
// You can interact with request.headers as an indexed variablerequest.headers["x-my-new-header"] = 42.to_string(); // Inserts a new header "x-my-new-header" with value "42"print(`${request.headers["x-my-new-header"]}`); // Writes "42" into the router log at info level// Rhai also supports extended dot notation for indexed variables, so this is equivalentrequest.headers.x-my-new-header = 42.to_string();// You can also set an header value from an array. Useful with the "set-cookie" header,// Note: It's probably more useful to do this on response headers. Simply illustrating the syntax here.request.headers["set-cookie"] = ["foo=bar; Domain=localhost; Path=/; Expires=Wed, 04 Jan 2023 17:25:27 GMT; HttpOnly; Secure; SameSite=None","foo2=bar2; Domain=localhost; Path=/; Expires=Wed, 04 Jan 2023 17:25:27 GMT; HttpOnly; Secure; SameSite=None",];// You can also get multiple header values for a header using the values() fn// Note: It's probably more useful to do this on response headers. Simply illustrating the syntax here.print(`${request.headers.values("set-cookie")}`);
request.method
这是客户端请求的 HTTP 方法。
print(`${request.method}`); // Log the HTTP method
request.body.query
这是客户端提供的 GraphQL 操作 字符串,用于执行。
要在查询规划 发生之前修改此值,必须在 supergraph_service()
请求回调中进行。如果稍后修改它,将使用原始提供的 operation 字符串生成查询规划。
以下示例修改了传入的 查询 并将其转换为完全无效的游戏:
print(`${request.body.query}`); // Log the query string before modificationrequest.body.query="query invalid { _typnam }}"; // Update the query string (in this case to an invalid query)print(`${request.body.query}`); // Log the query string after modification
request.body.operation_name
这是要执行的 GraphQL 操作的名称,如果请求中提供了名称。如果 request.body.query
包含多个操作定义,则此值 必须 存在。
以下是与operation_name
交互的示例,请参见examples/op-name-to-header目录。
print(`${request.body.operation_name}`); // Log the operation_name before modificationrequest.body.operation_name +="-my-suffix"; // Append "-my-suffix" to the operation_nameprint(`${request.body.operation_name}`); // Log the operation_name after modification
request.body.variables
这些是提供给操作的任何GraphQL变量的值。变量,它们以一个对象映射的形式暴露给Rhai。
print(`${request.body.variables}`); // Log all GraphQL variables
request.body.extensions
请求扩展可以读取或修改。它们以一个对象映射的形式暴露给Rhai。
print(`${request.body.extensions}`); // Log all extensions
request.uri.host
这是请求URI的主机组件,作为一个字符串。
修改客户端请求的此值没有任何效果,因为请求已经到达router。但是,在subgraph_service
回调中修改request.subgraph.uri.host
确实会修改router与相应subgraph通讯所使用的URI。
print(`${request.uri.host}`); // Log the request host
request.uri.path
这是请求URI的路径组件,作为一个字符串。
修改客户端请求的此值没有任何效果,因为请求已经到达router。但是,在subgraph_service
回调中修改request.subgraph.uri.path
确实会修改router与相应subgraph通讯所使用的URI。
print(`${request.uri.path}`); // log the request pathrequest.uri.path += "/added-context"; // Add an extra element to the query path
request.subgraph.*
仅在对subgraph_service
中注册的map_request
回调中,才可使用request.subgraph
对象。该对象与request
对象具有完全相同的fields,但这些fields适用于router将发送到相应subgraph的HTTP请求。
// You can interact with request.subgraph.headers as an indexed variablerequest.subgraph.headers["x-my-new-header"] = 42.to_string(); // Inserts a new header "x-my-new-header" with value "42"print(`${request.subgraph.headers["x-my-new-header"]}`); // Writes "42" into the router log at info level// Rhai also supports extended dot notation for indexed variables, so this is equivalentrequest.subgraph.headers.x-my-new-header = 42.to_string();
Response
接口
通过map_response
注册的所有回调函数都传入了表示HTTP响应的response
对象。
- 在
subgraph_service
中,该对象表示相应subgraph发送给router的响应。 - 在其他所有服务中,此对象表示路由器将发送给请求客户端的响应。
响应对象包含以下字段:
response.contextresponse.idresponse.status_coderesponse.headersresponse.body.labelresponse.body.dataresponse.body.errorsresponse.body.extensions
上述所有字段都是可读写。
以下字段的行为与其请求
对应字段相同:
response.is_primary()
在与响应上下文中的头部
或状态码
交互时请特别小心。对于router_service()
、supergraph_service()
和execution_service()
,在延迟响应流中的第一个响应才存在头部
和状态码
。您可以通过使用is_primary()
函数来处理这种情况,如果响应是第一个(或主要)响应,则它会返回true。如果您尝试访问非主要响应的头部
或状态码
,则将引发异常,这可以像处理任何其他Rhai异常一样处理,但不如使用is_primary()
方法方便。
if response.is_primary() {print(`all response headers: ${response.headers}`);} else {print(`don't try to access headers`);}
其他字段如下所述。
response.body.label
响应可能包含一个标签,可以将其作为字符串读取/写入。
print(`${response.body.label}`); // logs the response label
response.body.data
响应可能包含数据(有些带有错误的响应不包含数据)。在操作数据(和错误)时请小心,以确保响应保持有效。data
以对象映射的形式暴露给Rhai。
在examples/data-response-mutate目录中有一个与响应数据交互的完整示例。
print(`${response.body.data}`); // logs the response data
response.body.errors
响应可能包含错误。错误以Rhai中对象映射的数组形式表示。
每个错误必须包含至少以下内容:
- 消息(字符串)
- 位置(数组)
(位置可以是空数组。)
错误可选地还可以包含扩展,用作对象映射。
在examples/error-response-mutate目录中有一个与响应错误交互的完整示例。
// Create an error with our messagelet error_to_add = #{message: "this is an added error",locations: [],// Extensions are optional, adding some arbitrary extensions to illustrate syntaxextensions: #{field_1: "field 1",field_2: "field_2"}};// Add this error to any existing errorsresponse.body.errors += error_to_add;print(`${response.body.errors}`); // logs the response errors
response.status_code.to_string()
将响应状态码转换为字符串。
if response.status_code.to_string() == "200" {print(`ok`);}
如果要将响应状态码转换为数字,这也很有用
if parse_int(response.status_code.to_string()) == 200 {print(`ok`);}
您也可以从整数创建自己的状态码
if response.status_code == status_code_from_int(200) {print(`ok`);}
ⓘ 注意
`response.status_code` 对象仅对在 `router_service` 或 `subgraph_service` 中注册的 `map_response` 回调可用。