介绍

Async-graphql是用 Rust 语言实现的 GraphQL 服务端库。它完全兼容 GraphQL 规范以及绝大部分的扩展功能,类型安全并且高性能。

你可以用 Rust 语言的方式来定义 Schema,过程宏会自动生成 GraphQL 查询的框架代码,没有扩展 Rust 的语法,意味着 Rustfmt 可以正常使用,我很看重这一点,这也是为什么我会开发Async-graphql的原因之一。

为什么我要开发 Async-graphql?

我喜欢 GraphQL 和 Rust,之前我一直用Juniper,它解决了我用 Rust 实现 GraphQL 服务器的问题,但也有一些遗憾,其中最重要的是它当时不支持 async/await,所以我决定做一个给自己用。

快速开始

添加依赖

[dependencies]
async-graphql = "4.0"
async-graphql-actix-web = "4.0" # 如果你需要集成到 Actix-web
async-graphql-warp = "4.0" # 如果你需要集成到 Warp
async-graphql-tide = "4.0" # 如果你需要集成到 Tide

写一个 Schema

一个 GraphQL 的 Schema 包含一个必须的查询 (Query) 根对象,可选的变更 (Mutation) 根对象和可选的订阅 (Subscription) 根对象,这些对象类型都是用 Rust 语言的结构来描述它们,结构的字段对应 GraphQL 对象的字段。

Async-graphql 实现了常用数据类型到 GraphQL 类型的映射,例如i32, f64, Option<T>, Vec<T>等。同时,你也能够扩展这些基础类型,基础数据类型在 GraphQL 里面称为标量。

下面是一个简单的例子,我们只提供一个查询,返回ab的和。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Query;

#[Object]
impl Query {
    /// Returns the sum of a and b
    async fn add(&self, a: i32, b: i32) -> i32 {
        a + b
    }
}

}

执行查询

在我们这个例子里面,只有 Query,没有 Mutation 和 Subscription,所以我们用EmptyMutationEmptySubscription来创建 Schema,然后调用Schema::execute来执行查询。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Query;
#[Object]
impl Query {
  async fn version(&self) -> &str { "1.0" }    
}
async fn other() {
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let res = schema.execute("{ add(a: 10, b: 20) }").await;
}
}

把查询结果输出为 JSON

let json = serde_json::to_string(&res);

和 Web Server 的集成

请参考 https://github.com/async-graphql/examples。

类型系统

Async-graphql包含 GraphQL 类型到 Rust 类型的完整实现,并且非常易于使用。

简单对象 (SimpleObject)

简单对象是把 Rust 结构的所有字段都直接映射到 GraphQL 对象,不支持定义单独的 Resolver 函数。

下面的例子定义了一个名称为 MyObject 的对象,包含字段abc由于标记为#[graphql(skip)],所以不会映射到 GraphQL。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

#[derive(SimpleObject)]
struct MyObject {
    /// Value a
    a: i32,
    
    /// Value b
    b: i32,

    #[graphql(skip)]
    c: i32,
}
}

泛型

如果你希望其它类型能够重用SimpleObject,则可以定义泛型的SimpleObject,并指定具体的类型。

在下面的示例中,创建了两种SimpleObject类型:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct SomeType { a: i32 }
#[derive(SimpleObject)]
struct SomeOtherType { a: i32 }
#[derive(SimpleObject)]
#[graphql(concrete(name = "SomeName", params(SomeType)))]
#[graphql(concrete(name = "SomeOtherName", params(SomeOtherType)))]
pub struct SomeGenericObject<T: OutputType> {
    field1: Option<T>,
    field2: String
}
}

注意:每个泛型参数必须实现OutputType,如上所示。

生成的 SDL 如下:

type SomeName {
  field1: SomeType
  field2: String!
}

type SomeOtherName {
  field1: SomeOtherType
  field2: String!
}

在其它Object中使用具体的泛型类型:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct SomeType { a: i32 }
#[derive(SimpleObject)]
struct SomeOtherType { a: i32 }
#[derive(SimpleObject)]
#[graphql(concrete(name = "SomeName", params(SomeType)))]
#[graphql(concrete(name = "SomeOtherName", params(SomeOtherType)))]
pub struct SomeGenericObject<T: OutputType> {
    field1: Option<T>,
    field2: String,
}
#[derive(SimpleObject)]
pub struct YetAnotherObject {
    a: SomeGenericObject<SomeType>,
    b: SomeGenericObject<SomeOtherType>,
}
}

你可以将多个通用类型传递给params(),并用逗号分隔。

复杂字段

有时 GraphQL 对象的大多数字段仅返回结构成员的值,但是少数字段需要计算。通常我们使用Object宏来定义这样一个 GraphQL 对象。

ComplexObject宏可以更漂亮的完成这件事,我们可以使用SimpleObject宏来定义 一些简单的字段,并使用ComplexObject宏来定义其他一些需要计算的字段。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
#[graphql(complex)] // 注意:如果你希望 ComplexObject 宏生效,complex 属性是必须的
struct MyObj {
    a: i32,
    b: i32,
}

#[ComplexObject]
impl MyObj {
    async fn c(&self) -> i32 {
        self.a + self.b     
    }
}
}

同时用于输入和输出

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject, InputObject)]
#[graphql(input_name = "MyObjInput")] // 注意:你必须用 input_name 属性为输入类型定义一个新的名称,否则将产生一个运行时错误。
struct MyObj {
    a: i32,
    b: i32,
}
}

对象 (Object)

和简单对象不同,对象必须为所有的字段定义 Resolver 函数,Resolver 函数定义在 impl 块中。

一个 Resolver 函数必须是异步的,它的第一个参数必须是&self,第二个参数是可选的Context,接下来是字段的参数。

Resolver 函数用于计算字段的值,你可以执行一个数据库查询,并返回查询结果。函数的返回值是字段的类型,你也可以返回一个async_graphql::Result类型,这样能够返回一个错误,这个错误信息将输出到查询结果中。

在查询数据库时,你可能需要一个数据库连接池对象,这个对象是个全局的,你可以在创建 Schema 的时候,用SchemaBuilder::data函数设置Schema数据,用Context::data函数设置Context数据。下面的value_from_db字段展示了如何从Context中获取一个数据库连接。

#![allow(unused)]
fn main() {
extern crate async_graphql;
struct Data { pub name: String }
struct DbConn {}
impl DbConn {
  fn query_something(&self, id: i64) -> std::result::Result<Data, String> { Ok(Data {name:"".into()})}
}
struct DbPool {}
impl DbPool {
  fn take(&self) -> DbConn { DbConn {} }    
}
use async_graphql::*;

struct MyObject {
    value: i32,
}

#[Object]
impl MyObject {
    async fn value(&self) -> String {
        self.value.to_string()
    }

    async fn value_from_db(
        &self,
        ctx: &Context<'_>,
        #[graphql(desc = "Id of object")] id: i64
    ) -> Result<String> {
        let conn = ctx.data::<DbPool>()?.take();
        Ok(conn.query_something(id)?.name)
    }
}
}

查询上下文 (Context)

Context的主要目标是获取附加到Schema的全局数据或者与正在处理的实际查询相关的数据。

存储数据

Context中你可以存放全局数据,例如环境变量、数据库连接池,以及你在每个查询中可能需要的任何内容。

数据必须实现SendSync

你可以通过调用ctx.data::<TypeOfYourData>()来获取查询中的数据。

主意:如果 Resolver 函数的返回值是从Context中借用的,则需要明确说明参数的生命周期。

下面的例子展示了如何从Context中借用数据。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Query;

#[Object]
impl Query {
    async fn borrow_from_context_data<'ctx>(
        &self,
        ctx: &Context<'ctx>
    ) -> Result<&'ctx String> {
        ctx.data::<String>()
    }
}
}

Schema 数据

你可以在创建Schema时将数据放入上下文中,这对于不会更改的数据非常有用,例如连接池。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(Default,SimpleObject)]
struct Query { version: i32}
struct EnvStruct;
let env_struct = EnvStruct;
struct S3Object;
let s3_storage = S3Object;
struct DBConnection;
let db_core = DBConnection;
let schema = Schema::build(Query::default(), EmptyMutation, EmptySubscription)
    .data(env_struct)
    .data(s3_storage)
    .data(db_core)
    .finish();
}

请求数据

你可以在执行请求时将数据放入上下文中,它对于身份验证数据很有用。

一个使用warp的小例子:

#![allow(unused)]
fn main() {
extern crate async_graphql;
extern crate async_graphql_warp;
extern crate warp;
use async_graphql::*;
use warp::{Filter, Reply};
use std::convert::Infallible;
#[derive(Default, SimpleObject)]
struct Query { name: String }
struct AuthInfo { pub token: Option<String> }
let schema = Schema::build(Query::default(), EmptyMutation, EmptySubscription).finish();
let schema_filter = async_graphql_warp::graphql(schema);
let graphql_post = warp::post()
  .and(warp::path("graphql"))
  .and(warp::header::optional("Authorization"))
  .and(schema_filter)
  .and_then( |auth: Option<String>, (schema, mut request): (Schema<Query, EmptyMutation, EmptySubscription>, async_graphql::Request)| async move {
    // Do something to get auth data from the header
    let your_auth_data = AuthInfo { token: auth };
    let response = schema
      .execute(
        request
         .data(your_auth_data)
      ).await;
      
    Ok::<_, Infallible>(async_graphql_warp::GraphQLResponse::from(response))
  });
}

HTTP 头

使用Context你还可以插入或添加 HTTP 头。

#![allow(unused)]
fn main() {
extern crate async_graphql;
extern crate http;
use ::http::header::ACCESS_CONTROL_ALLOW_ORIGIN;
use async_graphql::*;
struct Query;
#[Object]
impl Query {
    async fn greet(&self, ctx: &Context<'_>) -> String {
        // Headers can be inserted using the `http` constants
        let was_in_headers = ctx.insert_http_header(ACCESS_CONTROL_ALLOW_ORIGIN, "*");

        // They can also be inserted using &str
        let was_in_headers = ctx.insert_http_header("Custom-Header", "1234");

        // If multiple headers with the same key are `inserted` then the most recent
        // one overwrites the previous. If you want multiple headers for the same key, use
        // `append_http_header` for subsequent headers
        let was_in_headers = ctx.append_http_header("Custom-Header", "Hello World");

        String::from("Hello world")
    }
}
}

Selection / LookAhead

有时你想知道子查询中请求了哪些字段用于优化数据处理,则可以使用ctx.field()读取查询中的字段,它将提供一个SelectionField,允许你在当前字段和子字段之间导航。

如果要跨查询或子查询执行搜索,则不必使用 SelectionField 手动执行此操作,可以使用 ctx.look_ahead() 来执行选择。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

#[derive(SimpleObject)]
struct Detail {
    c: i32,
    d: i32,
}

#[derive(SimpleObject)]
struct MyObj {
    a: i32,
    b: i32,
    detail: Detail,
}

struct Query;

#[Object]
impl Query {
    async fn obj(&self, ctx: &Context<'_>) -> MyObj {
        if ctx.look_ahead().field("a").exists() {
            // This is a query like `obj { a }`
        } else if ctx.look_ahead().field("detail").field("c").exists() {
            // This is a query like `obj { detail { c } }`
        } else {
            // This query doesn't have `a`
        }
        unimplemented!()
    }
}
}

错误处理

Resolver 函数可以返回一个 Result 类型,以下是 Result 的定义:

type Result<T> = std::result::Result<T, Error>;

任何错误都能够被转换为Error,并且你还能扩展标准的错误信息。

下面是一个例子,解析一个输入的字符串到整数,当解析失败时返回错误,并且附加额外的错误信息。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use std::num::ParseIntError;
use async_graphql::*;

struct Query;

#[Object]
impl Query {
    async fn parse_with_extensions(&self, input: String) -> Result<i32> {
        Ok("234a"
            .parse()
            .map_err(|err: ParseIntError| err.extend_with(|_, e| e.set("code", 400)))?)
    }
}
}

合并对象 (MergedObject)

为同一类型实现多次 Object

通常我们在 Rust 中可以为同一类型创建多个实现,但由于过程宏的限制,无法为同一个类型创建多个 Object 实现。例如,下面的代码将无法通过编译。

#[Object]
impl MyObject {
    async fn field1(&self) -> i32 {
        todo!()
    }
}

#[Object]
impl MyObject {
    async fn field2(&self) -> i32 {
        todo!()    
    }
}

#[derive(MergedObject)] 宏允许你合并多个独立的 Object 为一个。

提示: 每个#[Object]需要一个唯一的名称,即使在一个MergedObject内,所以确保每个对象有单独的名称。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct User { a: i32 }
#[derive(SimpleObject)]
struct Movie { a: i32 }
#[derive(Default)]
struct UserQuery;

#[Object]
impl UserQuery {
    async fn users(&self) -> Vec<User> {
        todo!()
    }
}

#[derive(Default)]
struct MovieQuery;

#[Object]
impl MovieQuery {
    async fn movies(&self) -> Vec<Movie> {
        todo!()
    }
}

#[derive(MergedObject, Default)]
struct Query(UserQuery, MovieQuery);

let schema = Schema::new(
    Query::default(),
    EmptyMutation,
    EmptySubscription
);
}

⚠️ 合并的对象无法在 Interface 中使用。

合并订阅

MergedObject一样,你可以派生MergedSubscription来合并单独的#[Subscription]块。

像合并对象一样,每个订阅块都需要一个唯一的名称。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use futures_util::stream::{Stream};
#[derive(Default,SimpleObject)]
struct Query { a: i32 }
#[derive(Default)]
struct Subscription1;

#[Subscription]
impl Subscription1 {
    async fn events1(&self) -> impl Stream<Item = i32> {
        futures_util::stream::iter(0..10)
    }
}

#[derive(Default)]
struct Subscription2;

#[Subscription]
impl Subscription2 {
    async fn events2(&self) -> impl Stream<Item = i32> {
        futures_util::stream::iter(10..20)
    }
}

#[derive(MergedSubscription, Default)]
struct Subscription(Subscription1, Subscription2);

let schema = Schema::new(
    Query::default(),
    EmptyMutation,
    Subscription::default()
);
}

派生字段

有时两个字段有一样的查询逻辑,仅仅是输出的类型不同,在 async-graphql 中,你可以为它创建派生字段。

在以下例子中,你已经有一个duration_rfc2822字段输出RFC2822格式的时间格式,然后复用它派生一个新的date_rfc3339字段。

#![allow(unused)]
fn main() {
extern crate chrono;
use chrono::Utc;
extern crate async_graphql;
use async_graphql::*;
struct DateRFC3339(chrono::DateTime<Utc>);
struct DateRFC2822(chrono::DateTime<Utc>);

#[Scalar]
impl ScalarType for DateRFC3339 {
  fn parse(value: Value) -> InputValueResult<Self> { todo!() } 

  fn to_value(&self) -> Value {
    Value::String(self.0.to_rfc3339())
  }
}

#[Scalar]
impl ScalarType for DateRFC2822 {
  fn parse(value: Value) -> InputValueResult<Self> { todo!() } 

  fn to_value(&self) -> Value {
    Value::String(self.0.to_rfc2822())
  }
}

impl From<DateRFC2822> for DateRFC3339 {
    fn from(value: DateRFC2822) -> Self {
      DateRFC3339(value.0)
    }
}

struct Query;

#[Object]
impl Query {
    #[graphql(derived(name = "date_rfc3339", into = "DateRFC3339"))]
    async fn duration_rfc2822(&self, arg: String) -> DateRFC2822 {
        todo!()
    }
}
}

它将呈现为如下 GraphQL:

type Query {
	duration_rfc2822(arg: String): DateRFC2822!
	duration_rfc3339(arg: String): DateRFC3339!
}

包装类型

因为 孤儿规则,以下代码无法通过编译:

impl From<Vec<U>> for Vec<T> {
  ...
}

因此,你将无法为现有的包装类型结构(如VecOption)生成派生字段。 但是当你为 T 实现了 From<U> 后,你可以为 Vec<T> 实现 From<Vec<U>>,为 Option<T> 实现 From<Option<U>>. 使用 with 参数来定义一个转换函数,而不是用 Into::into

Example

#![allow(unused)]
fn main() {
extern crate serde;
use serde::{Serialize, Deserialize};
extern crate async_graphql;
use async_graphql::*;
#[derive(Serialize, Deserialize, Clone)]
struct ValueDerived(String);

#[derive(Serialize, Deserialize, Clone)]
struct ValueDerived2(String);

scalar!(ValueDerived);
scalar!(ValueDerived2);

impl From<ValueDerived> for ValueDerived2 {
    fn from(value: ValueDerived) -> Self {
        ValueDerived2(value.0)
    }
}

fn option_to_option<T, U: From<T>>(value: Option<T>) -> Option<U> {
    value.map(|x| x.into())
}

#[derive(SimpleObject)]
struct TestObj {
    #[graphql(derived(owned, name = "value2", into = "Option<ValueDerived2>", with = "option_to_option"))]
    pub value1: Option<ValueDerived>,
}
}

枚举 (Enum)

定义枚举相当简单,直接给出一个例子。

Async-graphql 会自动把枚举项的名称转换为 GraphQL 标准的大写加下划线形式,你也可以用name属性自已定义名称。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

/// One of the films in the Star Wars Trilogy
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
pub enum Episode {
    /// Released in 1977.
    NewHope,

    /// Released in 1980.
    Empire,

    /// Released in 1983.
    #[graphql(name="AAA")]
    Jedi,
}
}

封装外部枚举类型

Rust 的 孤儿规则 要求特质或您要实现特质的类型必须在相同的板条箱中定义,因此你不能向 GraphQL 公开外部枚举类型。为了创建Enum类型,一种常见的解决方法是创建一个新的与现有远程枚举类型同等的枚举。

#![allow(unused)]
fn main() {
extern crate async_graphql;
mod remote_crate { pub enum RemoteEnum { A, B, C } }
use async_graphql::*;

/// Provides parity with a remote enum type
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
pub enum LocalEnum {
    A,
    B,
    C,
}

/// Conversion interface from remote type to our local GraphQL enum type
impl From<remote_crate::RemoteEnum> for LocalEnum {
    fn from(e: remote_crate::RemoteEnum) -> Self {
        match e {
            remote_crate::RemoteEnum::A => Self::A,
            remote_crate::RemoteEnum::B => Self::B,
            remote_crate::RemoteEnum::C => Self::C,
        }
    }
}
}

该过程很繁琐,需要多个步骤才能使本地枚举和远程枚举保持同步。Async_graphql提供了一个方便的功能,可在派生Enum之后通过附加属性生成 LocalEnum 的From <remote_crate::RemoteEnum>以及相反的From<LocalEnum> for remote_crate::RemoteEnum:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
mod remote_crate { pub enum RemoteEnum { A, B, C } }
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
#[graphql(remote = "remote_crate::RemoteEnum")]
enum LocalEnum {
    A,
    B,
    C,
}
}

接口 (Interface)

接口用于抽象具有特定字段集合的对象,Async-graphql内部实现实际上是一个包装器,包装器转发接口上定义的 Resolver 函数到实现该接口的对象,所以接口类型所包含的字段类型,参数都必须和实现该接口的对象完全匹配。

Async-graphql自动实现了对象到接口的转换,把一个对象类型转换为接口类型只需要调用Into::into

接口字段的name属性表示转发的 Resolver 函数,并且它将被转换为驼峰命名作为字段名称。 如果你需要自定义 GraphQL 接口字段名称,可以同时使用namemethod属性。

  • namemethod属性同时存在时,name是 GraphQL 接口字段名,而method是 Resolver 函数名。
  • 当只有name存在时,转换为驼峰命名后的name是 GraphQL 接口字段名,而name是 Resolver 函数名。
#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Circle {
    radius: f32,
}

#[Object]
impl Circle {
    async fn area(&self) -> f32 {
        std::f32::consts::PI * self.radius * self.radius
    }

    async fn scale(&self, s: f32) -> Shape {
        Circle { radius: self.radius * s }.into()
    }

    #[graphql(name = "short_description")]
    async fn short_description(&self) -> String {
        "Circle".to_string()
    }
}

struct Square {
    width: f32,
}

#[Object]
impl Square {
    async fn area(&self) -> f32 {
        self.width * self.width
    }

    async fn scale(&self, s: f32) -> Shape {
        Square { width: self.width * s }.into()
    }

    #[graphql(name = "short_description")]
    async fn short_description(&self) -> String {
        "Square".to_string()
    }
}

#[derive(Interface)]
#[graphql(
    field(name = "area", ty = "f32"),
    field(name = "scale", ty = "Shape", arg(name = "s", ty = "f32")),
    field(name = "short_description", method = "short_description", ty = "String")
)]
enum Shape {
    Circle(Circle),
    Square(Square),
}
}

手工注册接口类型

Async-graphql在初始化阶段从Schema开始遍历并注册所有被直接或者间接引用的类型,如果某个接口没有被引用到,那么它将不会存在于注册表中,就像下面的例子, 即使MyObject实现了MyInterface,但由于Schema中并没有引用MyInterface,类型注册表中将不会存在MyInterface类型的信息。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(Interface)]
#[graphql(
    field(name = "name", ty = "String"),
)]
enum MyInterface {
    MyObject(MyObject),
}

#[derive(SimpleObject)]
struct MyObject {
    name: String,
}

struct Query;

#[Object]
impl Query {
    async fn obj(&self) -> MyObject {
        todo!()
    }
}

type MySchema = Schema<Query, EmptyMutation, EmptySubscription>;
}

你需要在构造 Schema 时手工注册MyInterface类型:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(Interface)]
#[graphql(field(name = "name", ty = "String"))]
enum MyInterface { MyObject(MyObject) }
#[derive(SimpleObject)]
struct MyObject { name: String, }
struct Query;
#[Object]
impl Query { async fn version(&self) -> &str { "1.0" } }

Schema::build(Query, EmptyMutation, EmptySubscription)
    .register_output_type::<MyInterface>()
    .finish();
}

联合 (Union)

联合的定义和接口非常像,但不允许定义字段。这两个类型的实现原理也差不多,对于Async-graphql来说,联合类型是接口类型的子集。

下面把接口定义的例子做一个小小的修改,去掉字段的定义。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Circle {
    radius: f32,
}

#[Object]
impl Circle {
    async fn area(&self) -> f32 {
        std::f32::consts::PI * self.radius * self.radius
    }

    async fn scale(&self, s: f32) -> Shape {
        Circle { radius: self.radius * s }.into()
    }
}

struct Square {
    width: f32,
}

#[Object]
impl Square {
    async fn area(&self) -> f32 {
        self.width * self.width
    }

    async fn scale(&self, s: f32) -> Shape {
        Square { width: self.width * s }.into()
    }
}

#[derive(Union)]
enum Shape {
    Circle(Circle),
    Square(Square),
}
}

展平嵌套联合

GraphQL 的有个限制是Union类型内不能包含其它联合类型。所有成员必须为Object。 位置支持嵌套Union,我们可以用#graphql(flatten),是它们合并到上级Union类型。

#![allow(unused)]
fn main() {
extern crate async_graphql;
#[derive(async_graphql::Union)]
pub enum TopLevelUnion {
    A(A),

    // 除非我们使用 `flatten` 属性,否则将无法编译
    #[graphql(flatten)]
    B(B),
}

#[derive(async_graphql::SimpleObject)]
pub struct A {
    a: i32,
    // ...
}

#[derive(async_graphql::Union)]
pub enum B {
    C(C),
    D(D),
}

#[derive(async_graphql::SimpleObject)]
pub struct C {
    c: i32,
    // ...
}

#[derive(async_graphql::SimpleObject)]
pub struct D {
    d: i32,
    // ...
}
}

上面的示例将顶级Union转换为以下等效形式:

#![allow(unused)]
fn main() {
extern crate async_graphql;
#[derive(async_graphql::SimpleObject)]
struct A { a: i32 }
#[derive(async_graphql::SimpleObject)]
struct C { c: i32 }
#[derive(async_graphql::SimpleObject)]
struct D { d: i32 }
#[derive(async_graphql::Union)]
pub enum TopLevelUnion {
    A(A),
    C(C),
    D(D),
}
}

输入对象 (InputObject)

你可以定义一个对象作为参数类型,GraphQL 称之为Input Object,输入对象的定义方式和简单对象很像,不同的是,简单对象只能用于输出,而输入对象只能用于输入。

你也通过可选的#[graphql]属性来给字段添加描述,重命名。

#![allow(unused)]
fn main() {
extern crate async_graphql;
#[derive(SimpleObject)]
struct User { a: i32 }
use async_graphql::*;

#[derive(InputObject)]
struct Coordinate {
    latitude: f64,
    longitude: f64,
}

struct Mutation;

#[Object]
impl Mutation {
    async fn users_at_location(&self, coordinate: Coordinate, radius: f64) -> Vec<User> {
        // 将坐标写入数据库
        // ...
      todo!()
    }
}
}

泛型

如果你希望其它类型能够重用InputObject,则可以定义泛型的InputObject,并指定具体的类型。

在下面的示例中,创建了两种InputObject类型:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(InputObject)]
struct SomeType { a: i32 }
#[derive(InputObject)]
struct SomeOtherType { a: i32 }
#[derive(InputObject)]
#[graphql(concrete(name = "SomeName", params(SomeType)))]
#[graphql(concrete(name = "SomeOtherName", params(SomeOtherType)))]
pub struct SomeGenericInput<T: InputType> {
    field1: Option<T>,
    field2: String
}
}

注意:每个泛型参数必须实现InputType,如上所示。

生成的 SDL 如下:

input SomeName {
  field1: SomeType
  field2: String!
}

input SomeOtherName {
  field1: SomeOtherType
  field2: String!
}

在其它InputObject中使用具体的泛型类型:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(InputObject)]
struct SomeType { a: i32 }
#[derive(InputObject)]
struct SomeOtherType { a: i32 }
#[derive(InputObject)]
#[graphql(concrete(name = "SomeName", params(SomeType)))]
#[graphql(concrete(name = "SomeOtherName", params(SomeOtherType)))]
pub struct SomeGenericInput<T: InputType> {
    field1: Option<T>,
    field2: String
}
#[derive(InputObject)]
pub struct YetAnotherInput {
    a: SomeGenericInput<SomeType>,
    b: SomeGenericInput<SomeOtherType>,
}
}

你可以将多个通用类型传递给params(),并用逗号分隔。

默认值

你可以为输入值类型定义默认值,下面展示了在不同类型上默认值的定义方法。

对象字段参数

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Query;

fn my_default() -> i32 {
    30
}

#[Object]
impl Query {
    // value 参数的默认值为 0,它会调用 i32::default()
    async fn test1(&self, #[graphql(default)] value: i32) -> i32 { todo!() }

    // value 参数的默认值为 10
    async fn test2(&self, #[graphql(default = 10)] value: i32) -> i32 { todo!() }

    // value 参数的默认值使用 my_default 函数的返回结果,值为 30
    async fn test3(&self, #[graphql(default_with = "my_default()")] value: i32) -> i32 { todo!() }
}
}

接口字段参数

#![allow(unused)]
fn main() {
extern crate async_graphql;
fn my_default() -> i32 { 5 }
struct MyObj;
#[Object]
impl MyObj {
   async fn test1(&self, value: i32) -> i32 { todo!() }
   async fn test2(&self, value: i32) -> i32 { todo!() }
   async fn test3(&self, value: i32) -> i32 { todo!() }
}
use async_graphql::*;

#[derive(Interface)]
#[graphql(
    field(name = "test1", ty = "i32", arg(name = "value", ty = "i32", default)),
    field(name = "test2", ty = "i32", arg(name = "value", ty = "i32", default = 10)),
    field(name = "test3", ty = "i32", arg(name = "value", ty = "i32", default_with = "my_default()")),
)]
enum MyInterface {
    MyObj(MyObj),
}
}

输入对象 (InputObject)

#![allow(unused)]
fn main() {
extern crate async_graphql;
fn my_default() -> i32 { 5 }
use async_graphql::*;

#[derive(InputObject)]
struct MyInputObject {
    #[graphql(default)]
    value1: i32,

    #[graphql(default = 10)]
    value2: i32,

    #[graphql(default_with = "my_default()")]
    value3: i32,
}
}

定义模式 (Schema)

在定义了基本的类型之后,需要定义一个模式把他们组合起来,模式由三种类型组成,查询对象,变更对象和订阅对象,其中变更对象和订阅对象是可选的。

当模式创建时,Async-graphql会遍历所有对象图,并注册所有类型。这意味着,如果定义了 GraphQL 对象但从未引用,那么此对象就不会暴露在模式中。

查询和变更

查询根对象

查询根对象是一个 GraphQL 对象,定义类似其它对象。查询对象的所有字段 Resolver 函数是并发执行的。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct User { a: i32 }

struct Query;

#[Object]
impl Query {
    async fn user(&self, username: String) -> Result<Option<User>> {
        // 在数据库中查找用户
       todo!()
    }
}

}

变更根对象

变更根对象也是一个 GraphQL,但变更根对象的执行是顺序的,只有第一个变更执行完成之后才会执行下一个。

下面的变更根对象提供用户注册和登录操作:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Mutation;

#[Object]
impl Mutation {
    async fn signup(&self, username: String, password: String) -> Result<bool> {
        // 用户注册
       todo!()
    }

    async fn login(&self, username: String, password: String) -> Result<String> {
        // 用户登录并生成 token
       todo!()
    }
}
}

订阅

订阅根对象和其它根对象定义稍有不同,它的 Resolver 函数总是返回一个 Stream 或者Result<Stream>,而字段参数通常作为数据的筛选条件。

下面的例子订阅一个整数流,它每秒产生一个整数,参数step指定了整数的步长,默认为 1。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use std::time::Duration;
use async_graphql::futures_util::stream::Stream;
use async_graphql::futures_util::StreamExt;
extern crate tokio_stream;
extern crate tokio;
use async_graphql::*;

struct Subscription;

#[Subscription]
impl Subscription {
    async fn integers(&self, #[graphql(default = 1)] step: i32) -> impl Stream<Item = i32> {
        let mut value = 0;
        tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(Duration::from_secs(1)))
            .map(move |_| {
                value += step;
                value
            })
    }
}
}

实用功能

字段守卫 (Field Guard)

你可以为Object, SimpleObject, ComplexObjectSubscription的字段定义守卫,它将在调用字段的 Resolver 函数前执行,如果失败则返回一个错误。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(Eq, PartialEq, Copy, Clone)]
enum Role {
    Admin,
    Guest,
}

struct RoleGuard {
    role: Role,
}

impl RoleGuard {
    fn new(role: Role) -> Self {
        Self { role }
    }
}

impl Guard for RoleGuard {
    async fn check(&self, ctx: &Context<'_>) -> Result<()> {
        if ctx.data_opt::<Role>() == Some(&self.role) {
            Ok(())
        } else {
            Err("Forbidden".into())
        }
    }
}
}

guard属性使用它:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(Eq, PartialEq, Copy, Clone)]
enum Role { Admin, Guest, }
struct RoleGuard { role: Role, }
impl RoleGuard { fn new(role: Role) -> Self { Self { role } } }
impl Guard for RoleGuard { async fn check(&self, ctx: &Context<'_>) -> Result<()> { todo!() } }
#[derive(SimpleObject)]
struct Query {
    /// 只允许 Admin 访问
    #[graphql(guard = "RoleGuard::new(Role::Admin)")]
    value1: i32,
    /// 允许 Admin 或者 Guest 访问
    #[graphql(guard = "RoleGuard::new(Role::Admin).or(RoleGuard::new(Role::Guest))")]
    value2: i32,
}
}

从参数中获取值

有时候守卫需要从字段参数中获取值,你需要像下面这样在创建守卫时传递该参数值:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct EqGuard {
    expect: i32,
    actual: i32,
}

impl EqGuard {
    fn new(expect: i32, actual: i32) -> Self {
        Self { expect, actual }
    }
}

impl Guard for EqGuard {
    async fn check(&self, _ctx: &Context<'_>) -> Result<()> {
        if self.expect != self.actual {
            Err("Forbidden".into())
        } else {
            Ok(())
        }
    }
}

struct Query;

#[Object]
impl Query {
    #[graphql(guard = "EqGuard::new(100, value)")]
    async fn get(&self, value: i32) -> i32 {
        value
    }
}
}

输入值校验器

Async-graphql内置了一些常用的校验器,你可以在对象字段的参数或者InputObject的字段上使用它们。

  • maximum=N 指定数字不能大于N
  • minimum=N 指定数字不能小于N
  • multiple_of=N 指定数字必须是N的倍数
  • max_items=N 指定列表的长度不能大于N
  • min_items=N 指定列表的长度不能小于N
  • max_length=N 字符串的长度不能大于N
  • min_length=N 字符串的长度不能小于N
  • chars_max_length=N 字符串中 unicode 字符的的数量不能小于N
  • chars_min_length=N 字符串中 unicode 字符的的数量不能大于N
  • email 有效的 email
  • url 有效的 url
  • ip 有效的 ip 地址
  • regex=RE 匹配正则表达式
#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Query;

#[Object]
impl Query {
    /// name 参数的长度必须大于等于 5,小于等于 10
    async fn input(&self, #[graphql(validator(min_length = 5, max_length = 10))] name: String) -> Result<i32> {
       todo!()    
    }
}
}

校验列表成员

你可以打开list属性表示校验器作用于一个列表内的所有成员:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Query;

#[Object]
impl Query {
    async fn input(&self, #[graphql(validator(list, max_length = 10))] names: Vec<String>) -> Result<i32> {
       todo!()
    }
}
}

自定义校验器

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct MyValidator {
    expect: i32,
}

impl MyValidator {
    pub fn new(n: i32) -> Self {
        MyValidator { expect: n }
    }
}

impl CustomValidator<i32> for MyValidator {
    fn check(&self, value: &i32) -> Result<(), InputValueError<i32>> {
        if *value == self.expect {
            Ok(())
        } else {
            Err(InputValueError::custom(format!("expect 100, actual {}", value)))
        }
    }
}

struct Query;

#[Object]
impl Query {
    /// n 的值必须等于 100
    async fn value(
        &self,
        #[graphql(validator(custom = "MyValidator::new(100)"))] n: i32,
    ) -> i32 {
        n
    }
}
}

查询缓存控制

生产环境下通常依赖缓存来提高性能。

一个 GraphQL 查询会调用多个 Resolver 函数,每个 Resolver 函数都能够具有不同的缓存定义。有的可能缓存几秒钟,有的可能缓存几个小时,有的可能所有用户都相同,有的可能每个会话都不同。

Async-graphql提供一种机制允许定义结果的缓存时间和作用域。

你可以在对象上定义缓存参数,也可以在字段上定义,下面的例子展示了缓存控制参数的两种用法。

你可以用max_age参数来控制缓存时长(单位是秒),也可以用publicprivate来控制缓存的作用域,当你不指定时,作用域默认是public

Async-graphql查询时会合并所有缓存控制指令的结果,max_age取最小值。如果任何对象或者字段的作用域为private,则其结果的作用域为private,否则为public

我们可以从查询结果QueryResponse中获取缓存控制合并结果,并且调用CacheControl::value来获取对应的 HTTP 头。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct Query;
#[Object(cache_control(max_age = 60))]
impl Query {
    #[graphql(cache_control(max_age = 30))]
    async fn value1(&self) -> i32 {
        1
    }

    #[graphql(cache_control(private))]
    async fn value2(&self) -> i32 {
        2
    }

    async fn value3(&self) -> i32 {
        3
    }
}
}

下面是不同的查询对应不同缓存控制结果:

# max_age=30
{ value1 }
# max_age=30, private
{ value1 value2 }
# max_age=60
{ value3 }

游标连接 (Cursor Connections)

Relay 定义了一套游标连接规范,以提供一致性的分页查询方式,具体的规范文档请参考GraphQL Cursor Connections Specification

Async-graphql中定义一个游标连接非常简单,你只需要调用 connection::query 函数,并在闭包中查询数据。

下面是一个简单的获取连续整数的数据源:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::types::connection::*;

struct Query;

#[Object]
impl Query {
    async fn numbers(&self,
        after: Option<String>,
        before: Option<String>,
        first: Option<i32>,
        last: Option<i32>,
    ) -> Result<Connection<usize, i32, EmptyFields, EmptyFields>> {
        query(after, before, first, last, |after, before, first, last| async move {
            let mut start = after.map(|after| after + 1).unwrap_or(0);
            let mut end = before.unwrap_or(10000);
            if let Some(first) = first {
                end = (start + first).min(end);
            }
            if let Some(last) = last {
                start = if last > end - start {
                     end
                } else {
                    end - last
                };
            }
            let mut connection = Connection::new(start > 0, end < 10000);
            connection.edges.extend(
                (start..end).into_iter().map(|n|
                    Edge::with_additional_fields(n, n as i32, EmptyFields)
            ));
            Ok::<_, async_graphql::Error>(connection)
        }).await
    }
}

}

# 错误扩展

引用 graphql-spec

GraphQL 服务可以通过扩展提供错误的附加条目。 该条目(如果设置)必须是一个映射作为其值,用于附加错误的其它信息。

示例

我建议您查看此 错误扩展示例 作为快速入门。

一般概念

Async-graphql中,所有面向用户的错误都强制转换为Error类型,默认情况下会提供 由std:::fmt::Display暴露的错误消息。但是,Error实际上提供了一个额外的可以扩展错误的信息。

Resolver 函数类似这样:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct Query;
#[Object]
impl Query {
async fn parse_with_extensions(&self) -> Result<i32, Error> {
    Err(Error::new("MyMessage").extend_with(|_, e| e.set("details", "CAN_NOT_FETCH")))
}
}
}

然后可以返回如下响应:

{
  "errors": [
    {
      "message": "MyMessage",
      "locations": [ ... ],
      "path": [ ... ],
      "extensions": {
        "details": "CAN_NOT_FETCH",
      }
    }
  ]
}

ErrorExtensions

手动构造新的Error很麻烦。这就是为什么Async-graphql提供 两个方便特性,可将您的错误转换为适当的Error扩展。

扩展任何错误的最简单方法是对错误调用extend_with。 这将把任何错误转换为具有给定扩展信息的Error

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct Query;
use std::num::ParseIntError;
#[Object]
impl Query {
async fn parse_with_extensions(&self) -> Result<i32> {
     Ok("234a"
         .parse()
         .map_err(|err: ParseIntError| err.extend_with(|_err, e| e.set("code", 404)))?)
}
}
}

为自定义错误实现 ErrorExtensions

你也可以给自己的错误类型实现ErrorExtensions:

#![allow(unused)]
fn main() {
extern crate async_graphql;
extern crate thiserror;
use async_graphql::*;
#[derive(Debug, thiserror::Error)]
pub enum MyError {
    #[error("Could not find resource")]
    NotFound,

    #[error("ServerError")]
    ServerError(String),

    #[error("No Extensions")]
    ErrorWithoutExtensions,
}

impl ErrorExtensions for MyError {
    // lets define our base extensions
    fn extend(&self) -> Error {
        Error::new(format!("{}", self)).extend_with(|err, e| 
            match self {
              MyError::NotFound => e.set("code", "NOT_FOUND"),
              MyError::ServerError(reason) => e.set("reason", reason.clone()),
              MyError::ErrorWithoutExtensions => {}
          })
    }
}
}

您只需要对错误调用extend即可将错误与其提供的扩展信息一起传递,或者通过extend_with进一步扩展错误信息。

#![allow(unused)]
fn main() {
extern crate async_graphql;
extern crate thiserror;
use async_graphql::*;
#[derive(Debug, thiserror::Error)]
pub enum MyError {
    #[error("Could not find resource")]
    NotFound,

    #[error("ServerError")]
    ServerError(String),

    #[error("No Extensions")]
    ErrorWithoutExtensions,
}
struct Query;
#[Object]
impl Query {
async fn parse_with_extensions_result(&self) -> Result<i32> {
    // Err(MyError::NotFound.extend())
    // OR
    Err(MyError::NotFound.extend_with(|_, e| e.set("on_the_fly", "some_more_info")))
}
}
}
{
  "errors": [
    {
      "message": "NotFound",
      "locations": [ ... ],
      "path": [ ... ],
      "extensions": {
        "code": "NOT_FOUND",
        "on_the_fly": "some_more_info"
      }
    }
  ]
}

ResultExt

这个特质使您可以直接在结果上调用extend_err。因此上面的代码不再那么冗长。

// @todo figure out why this example does not compile!
extern crate async_graphql;
use async_graphql::*;
struct Query;
#[Object]
impl Query {
async fn parse_with_extensions(&self) -> Result<i32> {
     Ok("234a"
         .parse()
         .extend_err(|_, e| e.set("code", 404))?)
}
}

链式调用

由于对所有&E where E: std::fmt::Display实现了ErrorExtensionsResultsExt,我们可以将扩展链接在一起。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct Query;
#[Object]
impl Query {
async fn parse_with_extensions(&self) -> Result<i32> {
    match "234a".parse() {
        Ok(n) => Ok(n),
        Err(e) => Err(e
            .extend_with(|_, e| e.set("code", 404))
            .extend_with(|_, e| e.set("details", "some more info.."))
            // keys may also overwrite previous keys...
            .extend_with(|_, e| e.set("code", 500))),
    }
}
}
}

响应:

{
  "errors": [
    {
      "message": "MyMessage",
      "locations": [ ... ],
      "path": [ ... ],
      "extensions": {
      	"details": "some more info...",
        "code": 500,
      }
    }
  ]
}

缺陷

Rust 的稳定版本还未提供特化功能,这就是为什么ErrorExtensions&E where E: std::fmt::Display实现,代替E:std::fmt::Display通过提供一些特化功能。

Autoref-based stable specialization.

缺点是下面的代码不能编译:

async fn parse_with_extensions_result(&self) -> Result<i32> {
    // the trait `error::ErrorExtensions` is not implemented
    // for `std::num::ParseIntError`
    "234a".parse().extend_err(|_, e| e.set("code", 404))
}

但这可以通过编译:

async fn parse_with_extensions_result(&self) -> Result<i32> {
    // does work because ErrorExtensions is implemented for &ParseIntError
    "234a"
      .parse()
      .map_err(|ref e: ParseIntError| e.extend_with(|_, e| e.set("code", 404)))
}

Apollo Tracing 支持

Apollo Tracing提供了查询每个步骤的性能分析结果,它是一个Schema扩展,性能分析结果保存在QueryResponse中。

启用Apollo Tracing扩展需要在创建Schema的时候添加该扩展。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::extensions::ApolloTracing;

struct Query;
#[Object]
impl Query { async fn version(&self) -> &str { "1.0" } }

let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
    .extension(ApolloTracing) // 启用 ApolloTracing 扩展
    .finish();

}

查询的深度和复杂度

⚠️GraphQL 提供了非常灵活的查询方法,但在客户端上滥用复杂的查询可能造成风险,限制查询语句的深度和复杂度可以减轻这种风险。

昂贵的查询

考虑一种允许列出博客文章的架构。每个博客帖子也与其他帖子相关。

type Query {
	posts(count: Int = 10): [Post!]!
}

type Post {
	title: String!
	text: String!
	related(count: Int = 10): [Post!]!
}

创建一个会引起很大响应的查询不是很困难:

{
    posts(count: 100) {
        related(count: 100) {
            related(count: 100) {
                related(count: 100) {
                    title
                }
            }
        }
    }
}

响应的大小随related字段的每个其他级别呈指数增长。幸运的是,Async-graphql提供了一种防止此类查询的方法。

限制查询的深度

查询的深度是字段嵌套的层数,下面是一个深度为3的查询。

{
    a {
        b {
            c
        }
    }
}

在创建Schema的时候可以限制深度,如果查询语句超过这个限制,则会出错并且返回Query is nested too deep.消息。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct Query;
#[Object]
impl Query { async fn version(&self) -> &str { "1.0" } }
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
    .limit_depth(5) // 限制最大深度为 5
    .finish();
}

限制查询的复杂度

复杂度是查询语句中字段的数量,每个字段的复杂度默认为1,下面是一个复杂度为6的查询。

{
    a b c {
        d {
            e f
        }
    }
}

在创建Schema的时候可以限制复杂度,如果查询语句超过这个限制,则会出错并且返回Query is too complex.

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct Query;
#[Object]
impl Query { async fn version(&self) -> &str { "1.0" } }
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
    .limit_complexity(5) // 限制最大深度为 5
    .finish();
}

自定义字段的复杂度

针对非列表类型和列表类型的字段,有两种自定义复杂度的方法。 下面的代码中,value字段的复杂度为5。而values字段的复杂度为count * child_complexitychild_complexity是一个特殊的变量,表示子查询的复杂度, count是字段的参数,这个表达式用于计算values字段的复杂度,并且返回值的类型必须是usize

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct Query;

#[Object]
impl Query {
    #[graphql(complexity = 5)]
    async fn value(&self) -> i32 {
        todo!()
    }

    #[graphql(complexity = "count * child_complexity")]
    async fn values(&self, count: usize) -> i32 {
        todo!()
    }
}
}

注意:计算复杂度是在验证阶段完成而不是在执行阶段,所以你不用担心超限的查询语句会导致查询只执行一部分。

在内省中隐藏内容

默认情况下,所有类型,字段在内省中都是可见的。但可能你希望根据不同的用户来隐藏一些信息,避免引起不必要的误会。你可以在类型或者字段上添加visible属性来做到。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

#[derive(SimpleObject)]
struct MyObj {
    // 这个字段将在内省中可见
    a: i32,

    // 这个字段在内省中总是隐藏
    #[graphql(visible = false)]
    b: i32, 

    // 这个字段调用 `is_admin` 函数,如果函数的返回值为 `true` 则可见
    #[graphql(visible = "is_admin")]
    c: i32, 
}

#[derive(Enum, Copy, Clone, Eq, PartialEq)]
enum MyEnum {
    // 这个项目将在内省中可见
    A,

    // 这个项目在内省中总是隐藏
    #[graphql(visible = false)]
    B,

    // 这个项目调用 `is_admin` 函数,如果函数的返回值为 `true` 则可见
    #[graphql(visible = "is_admin")]
    C,
}

struct IsAdmin(bool);

fn is_admin(ctx: &Context<'_>) -> bool {
    ctx.data_unchecked::<IsAdmin>().0
}

}

扩展

async-graphql 允许你不修改核心代码就能扩展它功能。

如何定义扩展

async-graphql 扩展是通过实现 Extension trait 来定义的。 Extension trait 允许你将自定义代码插入到执行 GraphQL 查询的步骤中。

Extensions 很像来自其他框架的中间件,使用它们时要小心:当你使用扩展时它对每个 GraphQL 请求生效

一句话解释什么是中间件

让我们了解什么是中间件:

async fn middleware(&self, ctx: &ExtensionContext<'_>, next: NextMiddleware<'_>) -> MiddlewareResult {
  // 你的中间件代码

  /*
   * 调用 next.run 函数执行下个中间件的逻辑
   */
  next.run(ctx).await
}

如你所见,middleware 只是在末尾调用 next 函数的函数。但我们也可以在开头使用 next.run 来实现中间件。这就是它变得棘手的地方:根据你放置逻辑的位置以及next.run调用的位置,你的逻辑将不会具有相同的执行顺序。

根据你代码,你需要在 next.run 调用之前或之后处理它。如果你需要更多关于中间件的信息,网上有很多。

查询的处理

查询的每个阶段都有回调,你将能够基于这些回调创建扩展。

请求

首先,当我们收到一个请求时,如果它不是订阅,第一个被调用的函数将是 request,它在传入请求时调用,并输出结果给客户端。

Default implementation for request:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::extensions::*;
struct MyMiddleware;
#[async_trait::async_trait]
impl Extension for MyMiddleware {
async fn request(&self, ctx: &ExtensionContext<'_>, next: NextRequest<'_>) -> Response {
    next.run(ctx).await
}
}
}

根据你放置逻辑代码的位置,它将在正在查询执行的开头或结尾执行。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::extensions::*;
struct MyMiddleware;
#[async_trait::async_trait]
impl Extension for MyMiddleware {
async fn request(&self, ctx: &ExtensionContext<'_>, next: NextRequest<'_>) -> Response {
    // 此处的代码将在执行 prepare_request 之前运行。
    let result = next.run(ctx).await;
    // 此处的代码将在把结果发送给客户端之前执行
    result
}
}
}

准备查询

request 之后,将调用prepare_request,你可以在此处对请求做一些转换。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::*;
use async_graphql::extensions::*;
struct MyMiddleware;
#[async_trait::async_trait]
impl Extension for MyMiddleware {
async fn prepare_request(
    &self,
    ctx: &ExtensionContext<'_>,
    request: Request,
    next: NextPrepareRequest<'_>,
) -> ServerResult<Request> {
    // 此处的代码在 prepare_request 之前执行
    let result = next.run(ctx, request).await;
    // 此处的代码在 prepare_request 之后执行
    result
}
}
}

解析查询

parse_query 将解析查询语句并生成 GraphQL ExecutableDocument,并且检查查询是否遵循 GraphQL 规范。通常,async-graphql 遵循最后一个稳定的规范(October2021)。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::extensions::*;
use async_graphql::parser::types::ExecutableDocument;
struct MyMiddleware;
#[async_trait::async_trait]
impl Extension for MyMiddleware {
/// Called at parse query.
async fn parse_query(
    &self,
    ctx: &ExtensionContext<'_>,
    // The raw query
    query: &str,
    // The variables
    variables: &Variables,
    next: NextParseQuery<'_>,
) -> ServerResult<ExecutableDocument> {
    next.run(ctx, query, variables).await
}
}
}

校验

validation 步骤将执行查询校验(取决于你指定的 validation_mode),并向客户端提供有关查询无效的原因。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::extensions::*;
struct MyMiddleware;
#[async_trait::async_trait]
impl Extension for MyMiddleware {
/// Called at validation query.
async fn validation(
  &self,
  ctx: &ExtensionContext<'_>,
  next: NextValidation<'_>,
) -> Result<ValidationResult, Vec<ServerError>> {
  next.run(ctx).await
}
}
}

执行

execution 步骤是一个很大的步骤,它将并发执行Query,或者顺序执行Mutation

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::extensions::*;
struct MyMiddleware;
#[async_trait::async_trait]
impl Extension for MyMiddleware {
/// Called at execute query.
async fn execute(
    &self,
    ctx: &ExtensionContext<'_>,
    operation_name: Option<&str>,
    next: NextExecute<'_>,
) -> Response {
    // 此处的代码在执行完整查询之前执行
    let result = next.run(ctx, operation_name).await;
    // 此处的代码在执行完整查询之后执行
    result
}
}
}

resolve

为每个字段执行resolve.

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::extensions::*;
struct MyMiddleware;
#[async_trait::async_trait]
impl Extension for MyMiddleware { 
/// Called at resolve field.
async fn resolve(
    &self,
    ctx: &ExtensionContext<'_>,
    info: ResolveInfo<'_>,
    next: NextResolve<'_>,
) -> ServerResult<Option<Value>> {
    // resolve 字段之前
    let result = next.run(ctx, info).await;
    // resolve 字段之后
    result
}
}
}

订阅

subscribe的行为和request很像,只是专门用于订阅查询。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
use async_graphql::extensions::*;
use futures_util::stream::BoxStream;
struct MyMiddleware;
#[async_trait::async_trait]
impl Extension for MyMiddleware {
/// Called at subscribe request.
fn subscribe<'s>(
    &self,
    ctx: &ExtensionContext<'_>,
    stream: BoxStream<'s, Response>,
    next: NextSubscribe<'_>,
) -> BoxStream<'s, Response> {
    next.run(ctx, stream)
}
}
}

可用的扩展列表

async-graphql 中有很多可用的扩展用于增强你的 GraphQL 服务器。

Analyzer

Available in the repository

Analyzer 扩展将在每个响应的扩展中输出 complexitydepth 字段。

Apollo Persisted Queries

Available in the repository

要提高大型查询的性能,你可以启用此扩展,每个查询语句都将与一个唯一 ID 相关联,因此客户端可以直接发送此 ID 查询以减少请求的大小。

这个扩展不会强迫你使用一些缓存策略,你可以选择你想要的缓存策略,你只需要实现 CacheStorage trait:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[async_trait::async_trait]
pub trait CacheStorage: Send + Sync + Clone + 'static {
    /// Load the query by `key`.
    async fn get(&self, key: String) -> Option<String>;
    /// Save the query by `key`.
    async fn set(&self, key: String, query: String);
}
}

References: Apollo doc - Persisted Queries

Apollo Tracing

Available in the repository

Apollo Tracing 扩展用于在响应中包含此查询分析数据。此扩展程序遵循旧的且现已弃用的 Apollo Tracing Spec 。 如果你想支持更新的 Apollo Reporting Protocol,推荐使用 async-graphql Apollo studio extension

Apollo Studio

Available at async-graphql/async_graphql_apollo_studio_extension

async-graphql 提供了实现官方 Apollo Specification 的扩展,位于 async-graphql-extension- apollo-tracingcrates.io

Logger

Available in the repository

Logger 是一个简单的扩展,允许你向 async-graphql 添加一些日志记录功能。这也是学习如何创建自己的扩展的一个很好的例子。

OpenTelemetry

Available in the repository

OpenTelemetry 扩展提供 opentelemetry crate 的集成,以允许你的应用程序从 async-graphql 捕获分布式跟踪和指标。

Tracing

Available in the repository

Tracing 扩展提供 tracing crate 的集成,允许您向 async-graphql 添加一些跟踪功能,有点像Logger 扩展。

集成到 WebServer

Async-graphql提供了对一些常用 Web Server 的集成支持。

即使你目前使用的 Web Server 不在上面的列表中,自己实现类似的功能也相当的简单。

Poem

请求例子

#![allow(unused)]
fn main() {
extern crate async_graphql_poem;
extern crate async_graphql;
extern crate poem;
use async_graphql::*;
#[derive(Default, SimpleObject)]
struct Query { a: i32 }
let schema = Schema::build(Query::default(), EmptyMutation, EmptySubscription).finish();
use poem::Route;
use async_graphql_poem::GraphQL;

let app = Route::new()
    .at("/ws", GraphQL::new(schema));
}

订阅例子

#![allow(unused)]
fn main() {
extern crate async_graphql_poem;
extern crate async_graphql;
extern crate poem;
use async_graphql::*;
#[derive(Default, SimpleObject)]
struct Query { a: i32 }
let schema = Schema::build(Query::default(), EmptyMutation, EmptySubscription).finish();
use poem::{get, Route};
use async_graphql_poem::GraphQLSubscription;

let app = Route::new()
    .at("/ws", get(GraphQLSubscription::new(schema)));
}

更多例子

https://github.com/async-graphql/examples/tree/master/poem

Warp

Async-graphql-warp提供了两个Filtergraphqlgraphql_subscription

graphql用于执行QueryMutation请求,它提取 GraphQL 请求,然后输出一个包含async_graphql::Schemaasync_graphql::Request元组,你可以在之后组合其它 Filter,或者直接调用Schema::execute执行查询。

graphql_subscription用于实现基于 Web Socket 的订阅,它输出warp::Reply

请求例子

#![allow(unused)]
fn main() {
extern crate async_graphql_warp;
extern crate async_graphql;
extern crate warp;
use async_graphql::*;
use std::convert::Infallible;
use warp::Filter;
struct QueryRoot;
#[Object]
impl QueryRoot { async fn version(&self) -> &str { "1.0" } }
async fn other() {
type MySchema = Schema<QueryRoot, EmptyMutation, EmptySubscription>;

let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription);
let filter = async_graphql_warp::graphql(schema).and_then(|(schema, request): (MySchema, async_graphql::Request)| async move {
    // 执行查询
    let resp = schema.execute(request).await;

    // 返回结果
    Ok::<_, Infallible>(async_graphql_warp::GraphQLResponse::from(resp))
});
warp::serve(filter).run(([0, 0, 0, 0], 8000)).await;
}
}

订阅例子

#![allow(unused)]
fn main() {
extern crate async_graphql_warp;
extern crate async_graphql;
extern crate warp;
use async_graphql::*;
use futures_util::stream::{Stream, StreamExt};
use std::convert::Infallible;
use warp::Filter;
struct SubscriptionRoot;
#[Subscription]
impl SubscriptionRoot {
  async fn tick(&self) -> impl Stream<Item = i32> {
    futures_util::stream::iter(0..10)
  }
}
struct QueryRoot;
#[Object]
impl QueryRoot { async fn version(&self) -> &str { "1.0" } }
async fn other() {
let schema = Schema::new(QueryRoot, EmptyMutation, SubscriptionRoot);
let filter = async_graphql_warp::graphql_subscription(schema);
warp::serve(filter).run(([0, 0, 0, 0], 8000)).await;
}
}

更多例子

https://github.com/async-graphql/examples/tree/master/warp

Actix-web

Async-graphql-actix-web提供了GraphQLRequest提取器用于提取GraphQL请求,和GraphQLResponse用于输出GraphQL响应。

GraphQLSubscription用于创建一个支持 Web Socket 订阅的 Actor。

请求例子

你需要把 Schema 传入actix_web::App作为全局数据。

#![allow(unused)]
fn main() {
extern crate async_graphql_actix_web;
extern crate async_graphql;
extern crate actix_web;
use async_graphql::*;
#[derive(Default,SimpleObject)]
struct Query { a: i32 }
let schema = Schema::build(Query::default(), EmptyMutation, EmptySubscription).finish();
use actix_web::{web, HttpRequest, HttpResponse};
use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse};
async fn index(
    schema: web::Data<Schema<Query, EmptyMutation, EmptySubscription>>,
    request: GraphQLRequest,
) -> web::Json<GraphQLResponse> {
    web::Json(schema.execute(request.into_inner()).await.into())
}
}

订阅例子

#![allow(unused)]
fn main() {
extern crate async_graphql_actix_web;
extern crate async_graphql;
extern crate actix_web;
use async_graphql::*;
#[derive(Default,SimpleObject)]
struct Query { a: i32 }
let schema = Schema::build(Query::default(), EmptyMutation, EmptySubscription).finish();
use actix_web::{web, HttpRequest, HttpResponse};
use async_graphql_actix_web::GraphQLSubscription;
async fn index_ws(
    schema: web::Data<Schema<Query, EmptyMutation, EmptySubscription>>,
    req: HttpRequest,
    payload: web::Payload,
) -> actix_web::Result<HttpResponse> {
    GraphQLSubscription::new(Schema::clone(&*schema)).start(&req, payload)
}
}

更多例子

https://github.com/async-graphql/examples/tree/master/actix-web

高级主题

自定义标量

Async-graphql已经内置了绝大部分常用的标量类型,同时你也能自定义标量。

实现Async-graphql::Scalar即可自定义一个标量,你只需要实现一个解析函数和输出函数。

下面的例子定义一个 64 位整数标量,但它的输入输出都是字符串。 (Async-graphql已经内置了对 64 位整数的支持,正是采用字符串作为输入输出)

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;


struct StringNumber(i64);

#[Scalar]
impl ScalarType for StringNumber {
    fn parse(value: Value) -> InputValueResult<Self> {
        if let Value::String(value) = &value {
            // 解析整数
            Ok(value.parse().map(StringNumber)?)
        } else {
            // 类型不匹配
            Err(InputValueError::expected_type(value))
        }
    }

    fn to_value(&self) -> Value {
        Value::String(self.0.to_string())
    }
}

}

使用scalar!宏定义标量

如果你的类型实现了serde :: Serializeserde :: Deserialize,那么可以使用此宏更简单地定义标量。

#![allow(unused)]
fn main() {
extern crate async_graphql;
extern crate serde;
use async_graphql::*;
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize)]
struct MyValue {
    a: i32,
    b: HashMap<String, i32>,     
}

scalar!(MyValue);

// 重命名为 `MV`.
// scalar!(MyValue, "MV");

// 重命名为 `MV` 并且添加描述。
// scalar!(MyValue, "MV", "This is my value");
}

优化查询(解决 N+1 问题)

您是否注意到某些 GraphQL 查询需要执行数百个数据库查询,这些查询通常包含重复的数据,让我们来看看为什么以及如何修复它。

查询解析

想象一下,如果您有一个简单的查询,例如:

query { todos { users { name } } }

实现User的 resolver 代码如下:

struct User {
    id: u64,
}

#[Object]
impl User {
    async fn name(&self, ctx: &Context<'_>) -> Result<String> {
        let pool = ctx.data_unchecked::<Pool<Postgres>>();
        let (name,): (String,) = sqlx::query_as("SELECT name FROM user WHERE id = $1")
            .bind(self.id)
            .fetch_one(pool)
            .await?;
        Ok(name)
    }
}

执行查询将调用Todos的 resolver,该 resolver 执行SELECT * FROM todo并返回 N 个Todo对象。然后对每个Todo对象同时调用User的 resolver 执行SELECT name FROM user where id = $1

例如:

SELECT id, todo, user_id FROM todo
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1
SELECT name FROM user WHERE id = $1

执行了多次SELECT name FROM user WHERE id = $1,并且,大多数Todo对象都属于同一个用户,我们需要优化这些代码!

Dataloader

我们需要对查询分组,并且排除重复的查询。Dataloader就能完成这个工作,facebook 给出了一个请求范围的批处理和缓存解决方案。

下面是使用DataLoader来优化查询请求的例子:

use async_graphql::*;
use async_graphql::dataloader::*;
use itertools::Itertools;
use std::sync::Arc;

struct UserNameLoader {
    pool: sqlx::Pool<Postgres>,
}

impl Loader<u64> for UserNameLoader {
    type Value = String;
    type Error = Arc<sqlx::Error>;

    async fn load(&self, keys: &[u64]) -> Result<HashMap<u64, Self::Value>, Self::Error> {
        let query = format!("SELECT name FROM user WHERE id IN ({})", keys.iter().join(","));
        Ok(sqlx::query_as(query)
            .fetch(&self.pool)
            .map_ok(|name: String| name)
            .map_err(Arc::new)
            .try_collect().await?)
    }
}

struct User {
    id: u64,
}

#[Object]
impl User {
    async fn name(&self, ctx: &Context<'_>) -> Result<String> {
        let loader = ctx.data_unchecked::<DataLoader<UserNameLoader>>();
        let name: Option<String> = loader.load_one(self.id).await?;
        name.ok_or_else(|| "Not found".into())
    }
}

要在 ctx 中获取 UserNameLoader,您必须将其和任务生成器(例如 async_std::task::spawn)注册到 Schema 中:

let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription)
    .data(DataLoader::new(
        UserNameLoader,
        async_std::task::spawn, // 或者 `tokio::spawn`
    ))
    .finish();

最终只需要两个查询语句,就查询出了我们想要的结果!

SELECT id, todo, user_id FROM todo
SELECT name FROM user WHERE id IN (1, 2, 3, 4)

同一个 Loader 支持多种数据类型

你可以为同一个Loader实现多种数据类型,就像下面这样:

struct PostgresLoader {
    pool: sqlx::Pool<Postgres>,
}

impl Loader<UserId> for PostgresLoader {
    type Value = User;
    type Error = Arc<sqlx::Error>;

    async fn load(&self, keys: &[UserId]) -> Result<HashMap<UserId, Self::Value>, Self::Error> {
        // 从数据库中加载 User
    }
}

impl Loader<TodoId> for PostgresLoader {
    type Value = Todo;
    type Error = sqlx::Error;

    async fn load(&self, keys: &[TodoId]) -> Result<HashMap<TodoId, Self::Value>, Self::Error> {
        // 从数据库中加载 Todo
    }
}

自定义指令

Async-graphql可以很方便的自定义指令,这可以扩展 GraphQL 的行为。

创建一个自定义指令,需要实现 CustomDirective trait,然后用Directive宏生成一个工厂函数,该函数接收指令的参数并返回指令的实例。

目前Async-graphql仅支持添加FIELD位置的指令。

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct ConcatDirective {
    value: String,
}

#[async_trait::async_trait]
impl CustomDirective for ConcatDirective {
    async fn resolve_field(&self, _ctx: &Context<'_>, resolve: ResolveFut<'_>) -> ServerResult<Option<Value>> {
        resolve.await.map(|value| {
            value.map(|value| match value {
                Value::String(str) => Value::String(str + &self.value),
                _ => value,
            })
        })
    }
}

#[Directive(location = "Field")]
fn concat(value: String) -> impl CustomDirective {
    ConcatDirective { value }
}
}

创建模式时注册指令:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
struct Query;
#[Object]
impl Query { async fn version(&self) -> &str { "1.0" } }
struct ConcatDirective { value: String, }
#[async_trait::async_trait]
impl CustomDirective for ConcatDirective {
  async fn resolve_field(&self, _ctx: &Context<'_>, resolve: ResolveFut<'_>) -> ServerResult<Option<Value>> { todo!() }
}
#[Directive(location = "Field")]
fn concat(value: String) -> impl CustomDirective { ConcatDirective { value } }
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
    .directive(concat)
    .finish();
}

Apollo Federation 集成

Apollo Federation是一个GraphQL网关,它可以组合多个 GraphQL 服务,允许每服务仅实现它负责的那一部分数据,参考官方文档

Async-graphql可以完全支持Apollo Federation的所有功能,但需要对Schema定义做一些小小的改造。

  • async_graphql::Objectasync_graphql::Interfaceextends属性声明这个类别是一个已有类型的扩充。

  • 字段的external属性声明这个字段定义来自其它服务。

  • 字段的provides属性用于要求网关提供的字段集。

  • 字段的requires属性表示解析该字段值需要依赖该类型的字段集。

实体查找函数

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct User { id: ID }
struct Query;

#[Object]
impl Query {
    #[graphql(entity)]
    async fn find_user_by_id(&self, id: ID) -> User {
        User { id }
    }

    #[graphql(entity)]
    async fn find_user_by_id_with_username(&self, #[graphql(key)] id: ID, username: String) -> User {
        User { id }
    }

    #[graphql(entity)]
    async fn find_user_by_id_and_username(&self, id: ID, username: String) -> User {
        User { id }
    }
}
}

注意这三个查找函数的不同,他们都是查找 User 对象。

  • find_user_by_id

    使用id查找User对象,User对象的 key 是id

  • find_user_by_id_with_username

    使用id查找User对象,User对象的 key 是id,并且请求User对象的username字段值。

  • find_user_by_id_and_username

    使用idusername查找User对象,User对象的 key 是idusername

完整的例子请参考 https://github.com/async-graphql/examples/tree/master/federation

定义复合主键

一个主键可以包含多个字段,什么包含嵌套字段,你可以用InputObject来实现一个嵌套字段的 Key 类型。

下面的例子中User对象的主键是key { a b }

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct User { id: i32 }
#[derive(InputObject)]
struct NestedKey {
  a: i32,
  b: i32,
}

struct Query;

#[Object]
impl Query {
  #[graphql(entity)]
  async fn find_user_by_key(&self, key: NestedKey) -> User {
    User { id: key.a }
  }
}
}