Introduction

Async-graphql is a GraphQL server-side library implemented in Rust. It is fully compatible with the GraphQL specification and most of its extensions, and offers type safety and high performance.

You can define a Schema in Rust and procedural macros will automatically generate code for a GraphQL query. This library does not extend Rust's syntax, which means that Rustfmt can be used normally. I value this highly and it is one of the reasons why I developed Async-graphql.

Why do this?

I like GraphQL and Rust. I've been using Juniper, which solves the problem of implementing a GraphQL server with Rust. But Juniper had several problems, the most important of which is that it didn't support async/await at the time. So I decided to make this library for myself.

Benchmarks

Ensure that there is no CPU-heavy process in background!

cd benchmark
cargo bench

Now a HTML report is available at benchmark/target/criterion/report.

Quickstart

Add dependency libraries

[dependencies]
async-graphql = "4.0"
async-graphql-actix-web = "4.0" # If you need to integrate into actix-web
async-graphql-warp = "4.0" # If you need to integrate into warp
async-graphql-tide = "4.0" # If you need to integrate into tide

Write a Schema

The Schema of a GraphQL contains a required Query, an optional Mutation, and an optional Subscription. These object types are described using the structure of the Rust language. The field of the structure corresponds to the field of the GraphQL object.

Async-graphql implements the mapping of common data types to GraphQL types, such as i32, f64, Option<T>, Vec<T>, etc. Also, you can extend these base types, which are called scalars in the GraphQL.

Here is a simple example where we provide just one query that returns the sum of a and b.

#![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
    }
}
}

Execute the query

In our example, there is only a Query without a Mutation or Subscription, so we create the Schema with EmptyMutation and EmptySubscription, and then call Schema::execute to execute the Query.

#![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;
}
}

Output the query results as JSON

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

Web server integration

All examples are in the sub-repository, located in the examples directory.

git submodule update # update the examples repo
cd examples && cargo run --bin [name]

For more information, see the sub-repository README.md.

Type System

Async-graphql implements conversions from GraphQL Objects to Rust structs, and it's easy to use.

SimpleObject

SimpleObject directly maps all the fields of a struct to GraphQL object. If you don't require automatic mapping of fields, see Object.

The example below defines an object MyObject which includes the fields a and b. c will be not mapped to GraphQL as it is labelled as #[graphql(skip)]

#![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,
}
}

User-defined resolvers

Sometimes most of the fields of a GraphQL object simply return the value of the structure member, but a few fields are calculated. In this case, the Object macro cannot be used unless you hand-write all the resolvers.

The ComplexObject macro works in conjunction with the SimpleObject macro. The SimpleObject derive macro defines the non-calculated fields, where as the ComplexObject macro let's you write user-defined resolvers for the calculated fields.

Resolvers added to ComplexObject adhere to the same rules as resolvers of Object.

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
#[graphql(complex)] // NOTE: If you want the `ComplexObject` macro to take effect, this `complex` attribute is required.
struct MyObj {
    a: i32,
    b: i32,
}

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

Generic SimpleObjects

If you want to reuse a SimpleObject for other types, you can define a generic SimpleObject and specify how its concrete types should be implemented.

In the following example, two SimpleObject types are created:

#![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
}
}

Note: Each generic parameter must implement OutputType, as shown above.

The schema generated is:

type SomeName {
  field1: SomeType
  field2: String!
}

type SomeOtherName {
  field1: SomeOtherType
  field2: String!
}

In your resolver method or field of another object, use as a normal generic type:

#![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>,
}
}

You can pass multiple generic types to params(), separated by a comma.

Used for both input and output

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject, InputObject)]
#[graphql(input_name = "MyObjInput")] // Note: You must use the input_name attribute to define a new name for the input type, otherwise a runtime error will occur.
struct MyObj {
    a: i32,
    b: i32,
}
}

Flatten fields

You can flatten fields by adding #[graphql(flatten)], i.e.:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
pub struct ChildObject {
    b: String,
    c: String,
}

#[derive(SimpleObject)]
pub struct ParentObject {
    a: String,
    #[graphql(flatten)]
    child: ChildObject,
}

// Is the same as

#[derive(SimpleObject)]
pub struct Object {
    a: String,
    b: String,
    c: String,
}
}

Object

Different from SimpleObject, Object must have a resolver defined for each field in its impl.

A resolver function has to be asynchronous. The first argument has to be &self, the second is an optional Context and it is followed by field arguments.

The resolver is used to get the value of the field. For example, you can query a database and return the result. The return type of the function is the type of the field. You can also return a async_graphql::Result to return an error if it occurs. The error message will then be sent as query result.

You may need access to global data in your query, for example a database connection pool. When creating your Schema, you can use SchemaBuilder::data to configure the global data, and Context::data to configure Context data. The following value_from_db function shows how to retrieve a database connection from 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

The main goal of Context is to acquire global data attached to Schema and also data related to the actual query being processed.

Store Data

Inside the Context you can put global data, like environment variables, db connection pool, whatever you may need in every query.

The data must implement Send and Sync.

You can request the data inside a query by just calling ctx.data::<TypeOfYourData>().

Note that if the return value of resolver function is borrowed from Context, you will need to explicitly state the lifetime of the argument.

The following example shows how to borrow data in 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 data

You can put data inside the context at the creation of the schema, it's useful for data that do not change, like a connection pool.

An instance of how it would be written inside an application:

#![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();
}

Request data

You can put data inside the context at the execution of the request, it's useful for authentication data for instance.

A little example with a warp route:

#![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))
  });
}

Headers

With the Context you can also insert and appends headers.

#![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

Sometimes you want to know what fields are requested in the subquery to optimize the processing of data. You can read fields across the query with ctx.field() which will give you a SelectionField which will allow you to navigate across the fields and subfields.

If you want to perform a search across the query or the subqueries, you do not have to do this by hand with the SelectionField, you can use the ctx.look_ahead() to perform a selection

#![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!()
    }
}
}

Error handling

Resolve can return a Result, which has the following definition:

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

Any Error that implements std::fmt::Display can be converted to Error and you can extend the error message.

The following example shows how to parse an input string to an integer. When parsing fails, it will return an error and attach an error message. See the Error Extensions section of this book for more details.

#![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)))?)
    }
}
}

Errors in subscriptions

Errors can be returned from subscription resolvers as well, using a return type of the form:

async fn my_subscription_resolver(&self) -> impl Stream<Item = Result<MyItem, MyError>> { ... }

Note however that the MyError struct must have Clone implemented, due to the restrictions placed by the Subscription macro. One way to accomplish this is by creating a custom error type, with #[derive(Clone)], as seen here.

Merging Objects

Usually we can create multiple implementations for the same type in Rust, but due to the limitation of procedural macros, we can not create multiple Object implementations for the same type. For example, the following code will fail to compile.

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

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

Instead, the #[derive(MergedObject)] macro allows you to split an object's resolvers across multiple modules or files by merging 2 or more #[Object] implementations into one.

Tip: Every #[Object] needs a unique name, even in a MergedObject, so make sure to give each object you're merging its own name.

Note: This works for queries and mutations. For subscriptions, see "Merging Subscriptions" below.

#![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
);
}

⚠️ MergedObject cannot be used in Interface.

Merging Subscriptions

Along with MergedObject, you can derive MergedSubscription or use #[MergedSubscription] to merge separate #[Subscription] blocks.

Like merging Objects, each subscription block requires a unique name.

Example:

#![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()
);
}

Derived fields

Sometimes two fields have the same query logic, but the output type is different. In async-graphql, you can create a derived field for it.

In the following example, you already have a date_rfc2822 field outputting the time format in RFC2822 format, and then reuse it to derive a new date_rfc3339 field.

#![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 date_rfc2822(&self, arg: String) -> DateRFC2822 {
        todo!()
    }
}
}

It will render a GraphQL like:

type Query {
	date_rfc2822(arg: String): DateRFC2822!
	date_rfc3339(arg: String): DateRFC3339!
}

Wrapper types

A derived field won't be able to manage everything easily: Rust's orphan rule requires that either the trait or the type for which you are implementing the trait must be defined in the same crate as the impl, so the following code cannot be compiled:

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

So you wouldn't be able to generate derived fields for existing wrapper type structures like Vec or Option. But when you implement a From<U> for T you should be able to derived a From<Vec<U>> for Vec<T> and a From<Option<U>> for Option<T>. We included a with parameter to help you define a function to call instead of using the Into trait implementation between wrapper structures.

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

It's easy to define an Enum, here we have an example:

Async-graphql will automatically change the name of each item to GraphQL's CONSTANT_CASE convention. You can use name to rename.

#![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,
}
}

Wrapping a remote enum

Rust's orphan rule requires that either the trait or the type for which you are implementing the trait must be defined in the same crate as the impl, so you cannot expose remote enumeration types to GraphQL. In order to provide an Enum type, a common workaround is to create a new enum that has parity with the existing, remote enum type.

#![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,
        }
    }
}
}

The process is tedious and requires multiple steps to keep the local and remote enums in sync. Async_graphql provides a handy feature to generate the From<remote_crate::RemoteEnum> for LocalEnum as well as an opposite direction of From<LocalEnum> for remote_crate::RemoteEnum via an additional attribute after deriving Enum:

#![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

Interface is used to abstract Objects with common fields. Async-graphql implements it as a wrapper. The wrapper will forward field resolution to the Object that implements this Interface. Therefore, the Object's fields' type and arguments must match with the Interface's.

Async-graphql implements auto conversion from Object to Interface, you only need to call Into::into.

Interface field names are transformed to camelCase for the schema definition. If you need e.g. a snake_cased GraphQL field name, you can use both the name and method attributes.

  • When name and method exist together, name is the GraphQL field name and the method is the resolver function name.
  • When only name exists, name.to_camel_case() is the GraphQL field name and the name is the resolver function name.
#![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),
}
}

Register the interface manually

Async-graphql traverses and registers all directly or indirectly referenced types from Schema in the initialization phase. If an interface is not referenced, it will not exist in the registry, as in the following example , even if MyObject implements MyInterface, because MyInterface is not referenced in Schema, the MyInterface type will not exist in the registry.

#![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>;
}

You need to manually register the MyInterface type when constructing the Schema:

#![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

The definition of a Union is similar to an Interface, but with no fields allowed.. The implementation is quite similar for Async-graphql; from Async-graphql's perspective, Union is a subset of Interface.

The following example modified the definition of Interface a little bit and removed fields.

#![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),
}
}

Flattening nested unions

A restriction in GraphQL is the inability to create a union type out of other union types. All members must be Object. To support nested unions, we can "flatten" members that are unions, bringing their members up into the parent union. This is done by applying #[graphql(flatten)] on each member we want to flatten.

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

    // Will fail to compile unless we flatten the union member
    #[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,
    // ...
}
}

The above example transforms the top-level union into this equivalent:

#![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

You can use an Object as an argument, and GraphQL calls it an InputObject.

The definition of InputObject is similar to SimpleObject, but SimpleObject can only be used as output and InputObject can only be used as input.

You can add optional #[graphql] attributes to add descriptions or rename the field.

#![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> {
        // Writes coordination to database.
        // ...
      todo!()
    }
}
}

Generic InputObjects

If you want to reuse an InputObject for other types, you can define a generic InputObject and specify how its concrete types should be implemented.

In the following example, two InputObject types are created:

#![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
}
}

Note: Each generic parameter must implement InputType, as shown above.

The schema generated is:

input SomeName {
  field1: SomeType
  field2: String!
}

input SomeOtherName {
  field1: SomeOtherType
  field2: String!
}

In your resolver method or field of another input object, use as a normal generic type:

#![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>,
}
}

You can pass multiple generic types to params(), separated by a comma.

Redacting sensitive data

If any part of your input is considered sensitive and you wish to redact it, you can mark it with secret directive. For example:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(InputObject)]
pub struct CredentialsInput {
    username: String,
    #[graphql(secret)]
    password: String,
}
}

Flattening fields

You can add #[graphql(flatten)] to a field to inline keys from the field type into it's parent. For example:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(InputObject)]
pub struct ChildInput {
    b: String,
    c: String,
}

#[derive(InputObject)]
pub struct ParentInput {
    a: String,
    #[graphql(flatten)]
    child: ChildInput,
}

// Is the same as

#[derive(InputObject)]
pub struct Input {
    a: String,
    b: String,
    c: String,
}
}

OneofObject

A OneofObject is a special type of InputObject, in which only one of its fields must be set and is not-null. It is especially useful when you want a user to be able to choose between several potential input types.

This feature is still an RFC and therefore not yet officially part of the GraphQL spec, but Async-graphql already supports it!

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

#[derive(OneofObject)]
enum UserBy {
    Email(String),
    RegistrationNumber(i64),
    Address(Address)
}

#[derive(InputObject)]
struct Address {
    street: String,
    house_number: String,
    city: String,
    zip: String,
}

struct Query {}

#[Object]
impl Query {
    async fn search_users(&self, by: Vec<UserBy>) -> Vec<User> {
        // ... Searches and returns a list of users ...
        todo!()
    }
}
}

As you can see, a OneofObject is represented by an enum in which each variant contains another InputType. This means that you can use InputObject as variant too.

Default value

You can define default values for input value types. Below are some examples.

Object field

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

struct Query;

fn my_default() -> i32 {
    30
}

#[Object]
impl Query {
    // The default value of the value parameter is 0, it will call i32::default()
    async fn test1(&self, #[graphql(default)] value: i32) -> i32 { todo!() }

    // The default value of the value parameter is 10
    async fn test2(&self, #[graphql(default = 10)] value: i32) -> i32 { todo!() }
    
    // The default value of the value parameter uses the return result of the my_default function, the value is 30.
    async fn test3(&self, #[graphql(default_with = "my_default()")] value: i32) -> i32 { todo!() }
}
}

Interface field

#![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),
}
}

Input object field

#![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

After defining the basic types, you need to define a schema to combine them. The schema consists of three types: a query object, a mutation object, and a subscription object, where the mutation object and subscription object are optional.

When the schema is created, Async-graphql will traverse all object graphs and register all types. This means that if a GraphQL object is defined but never referenced, this object will not be exposed in the schema.

Query and Mutation

Query root object

The query root object is a GraphQL object with a definition similar to other objects. Resolver functions for all fields of the query object are executed concurrently.

#![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>> {
        // Look up users from the database
       todo!()
    }
}

}

Mutation root object

The mutation root object is also a GraphQL object, but it executes sequentially. One mutation following from another will only be executed only after the first mutation is completed.

The following mutation root object provides an example of user registration and login:

#![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> {
        // User signup
       todo!()
    }

    async fn login(&self, username: String, password: String) -> Result<String> {
        // User login (generate token)
       todo!()
    }
}
}

Subscription

The definition of the subscription root object is slightly different from other root objects. Its resolver function always returns a Stream or Result<Stream>, and the field parameters are usually used as data filtering conditions.

The following example subscribes to an integer stream, which generates one integer per second. The parameter step specifies the integer step size with a default of 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
            })
    }
}
}

SDL Export

You can export your schema in Schema Definition Language (SDL) by using the Schema::sdl() method.

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

struct Query;

#[Object]
impl Query {
    async fn add(&self, u: i32, v: i32) -> i32 {
        u + v
    }
}

let schema = Schema::build(Query, EmptyMutation, EmptySubscription).finish();
    
// Print the schema in SDL format
println!("{}", &schema.sdl());
}

Utilities

Field Guard

You can define a guard for the fields of Object, SimpleObject, ComplexObject and Subscription, it will be executed before calling the resolver function, and an error will be returned if it fails.

#![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())
        }
    }
}
}

Use it with the guard attribute:

#![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 {
    /// Only allow Admin
    #[graphql(guard = "RoleGuard::new(Role::Admin)")]
    value1: i32,
    /// Allow Admin or Guest
    #[graphql(guard = "RoleGuard::new(Role::Admin).or(RoleGuard::new(Role::Guest))")]
    value2: i32,
}
}

Use parameter value

Sometimes guards need to use field parameters, you need to pass the parameter value when creating the guard like this:

#![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
    }
}
}

Input value validators

Async-graphql has some common validators built-in, you can use them on the parameters of object fields or on the fields of InputObject.

  • maximum=N the number cannot be greater than N.
  • minimum=N the number cannot be less than N.
  • multiple_of=N the number must be a multiple of N.
  • max_items=N the length of the list cannot be greater than N.
  • min_items=N the length of the list cannot be less than N.
  • max_length=N the length of the string cannot be greater than N.
  • min_length=N the length of the string cannot be less than N.
  • chars_max_length=N the count of the unicode chars cannot be greater than N.
  • chars_min_length=N the count of the unicode chars cannot be less than N.
  • email is valid email.
  • url is valid url.
  • ip is valid ip address.
  • regex=RE is match for the regex.
#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;

struct Query;

#[Object]
impl Query {
    /// The length of the name must be greater than or equal to 5 and less than or equal to 10.
    async fn input(&self, #[graphql(validator(min_length = 5, max_length = 10))] name: String) -> Result<i32> {
        todo!()
    }
}
}

Check every member of the list

You can enable the list attribute, and the validator will check all members in 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!()
    }
}
}

Custom validator

#![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 must be equal to 100
    async fn value(
        &self,
        #[graphql(validator(custom = "MyValidator::new(100)"))] n: i32,
    ) -> i32 {
        n
    }
}
}

Cache control

Production environments often rely on caching to improve performance.

A GraphQL query will call multiple resolver functions and each resolver can have a different cache definition. Some may cache for a few seconds, some may cache for a few hours, some may be the same for all users, and some may be different for each session.

Async-graphql provides a mechanism that allows you to define the cache time and scope for each resolver.

You can define cache parameters on the object or on its fields. The following example shows two uses of cache control parameters.

You can use max_age parameters to control the age of the cache (in seconds), and you can also use public and private to control the scope of the cache. When you do not specify it, the scope will default to public.

when querying multiple resolvers, the results of all cache control parameters will be combined and the max_age minimum value will be taken. If the scope of any object or field is private, the result will be private.

We can use QueryResponse to get a merged cache control result from a query result, and call CacheControl::value to get the corresponding HTTP header.

#![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
    }
}
}

The following are different queries corresponding to different cache control results:

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

Cursor connections

Relay's cursor connection specification is designed to provide a consistent method for query paging. For more details on the specification see the GraphQL Cursor Connections Specification

Defining a cursor connection in async-graphql is very simple, you just call the connection::query function and query data in the closure.

#![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
    }
}

}

Error extensions

To quote the graphql-spec:

GraphQL services may provide an additional entry to errors with key extensions. This entry, if set, must have a map as its value. This entry is reserved for implementer to add additional information to errors however they see fit, and there are no additional restrictions on its contents.

Example

I would recommend on checking out this async-graphql example as a quickstart.

General Concept

In async-graphql all user-facing errors are cast to the Error type which by default provides the error message exposed by std::fmt::Display. However, Error actually provides an additional information that can extend the error.

A resolver looks like this:

#![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")))
}
}
}

may then return a response like this:

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

ErrorExtensions

Constructing new Errors by hand quickly becomes tedious. That is why async-graphql provides two convenience traits for casting your errors to the appropriate Error with extensions.

The easiest way to provide extensions to any error is by calling extend_with on the error. This will on the fly convert any error into a Error with the given extension.

#![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)))?)
}
}
}

Implementing ErrorExtensions for custom errors.

If you find yourself attaching extensions to your errors all over the place you might want to consider implementing the trait on your custom error type directly.

#![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 => {}
          })
    }
}
}

This way you only need to call extend on your error to deliver the error message alongside the provided extensions. Or further extend your error through 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

This trait enables you to call extend_err directly on results. So the above code becomes less verbose.

// @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))?)
}
}

Chained extensions

Since ErrorExtensions and ResultExt are implemented for any type &E where E: std::fmt::Display we can chain the extension together.

#![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))),
    }
}
}
}

Expected response:

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

Pitfalls

Rust does not provide stable trait specialization yet. That is why ErrorExtensions is actually implemented for &E where E: std::fmt::Display instead of E: std::fmt::Display. Some specialization is provided through Autoref-based stable specialization. The disadvantage is that the below code does NOT compile:

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

however this does:

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 provides performance analysis results for each step of query. This is an extension to Schema, and the performance analysis results are stored in QueryResponse.

To enable the Apollo Tracing extension, add the extension when the Schema is created.

#![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) // Enable ApolloTracing extension
    .finish();
}

Query complexity and depth

⚠️GraphQL provides a powerful way to query your data, but putting great power in the hands of your API clients also exposes you to a risk of denial of service attacks. You can mitigate that risk with Async-graphql by limiting the complexity and depth of the queries you allow.

Expensive Queries

Consider a schema that allows listing blog posts. Each blog post is also related to other posts.

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

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

It's not too hard to craft a query that will cause a very large response:

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

The size of the response increases exponentially with every other level of the related field. Fortunately, Async-graphql provides a way to prevent such queries.

Limiting Query depth

The depth is the number of nesting levels of the field, and the following is a query with a depth of 3.

{
    a {
        b {
            c
        }
    }
}

You can limit the depth when creating Schema. If the query exceeds this limit, an error will occur and the message Query is nested too deep will be returned.

#![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) // Limit the maximum depth to 5
    .finish();
}

Limiting Query complexity

The complexity is the number of fields in the query. The default complexity of each field is 1. Below is a query with a complexity of 6.

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

You can limit the complexity when creating the Schema. If the query exceeds this limit, an error will occur and Query is too complex will be returned.

#![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) // Limit the maximum complexity to 5
    .finish();
}

Custom Complexity Calculation

There are two ways to customize the complexity for non-list type and list type fields.

In the following code, the complexity of the value field is 5. The complexity of the values field is count * child_complexity, child_complexity is a special variable that represents the complexity of the subquery, and count is the parameter of the field, used to calculate the complexity of the values field, and the type of the return value must be 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!()
    }
}
}

Note: The complexity calculation is done in the validation phase and not the execution phase, so you don't have to worry about partial execution of over-limit queries.

Hide content in introspection

By default, all types and fields are visible in introspection. But maybe you want to hide some content according to different users to avoid unnecessary misunderstandings. You can add the visible attribute to the type or field to do it.

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

#[derive(SimpleObject)]
struct MyObj {
    // This field will be visible in introspection.
    a: i32,

    // This field is always hidden in introspection.
    #[graphql(visible = false)]
    b: i32,

    // This field calls the `is_admin` function, which 
    // is visible if the return value is `true`.
    #[graphql(visible = "is_admin")]
    c: i32,
}

#[derive(Enum, Copy, Clone, Eq, PartialEq)]
enum MyEnum {
    // This item will be visible in introspection.
    A,

    // This item is always hidden in introspection.
    #[graphql(visible = false)]
    B,

    // This item calls the `is_admin` function, which 
    // is visible if the return value is `true`.
    #[graphql(visible = "is_admin")]
    C,
}

struct IsAdmin(bool);

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

}

Extensions

async-graphql has the capability to be extended with extensions without having to modify the original source code. A lot of features can be added this way, and a lot of extensions already exist.

How extensions are defined

An async-graphql extension is defined by implementing the trait Extension associated. The Extension trait allows you to insert custom code to some several steps used to respond to GraphQL's queries through async-graphql. With Extensions, your application can hook into the GraphQL's requests lifecycle to add behaviors about incoming requests or outgoing response.

Extensions are a lot like middleware from other frameworks, be careful when using those: when you use an extension it'll be run for every GraphQL request.

Across every step, you'll have the ExtensionContext supplied with data about your current request execution. Feel free to check how it's constructed in the code, documentation about it will soon come.

A word about middleware

For those who don't know, let's dig deeper into what is a middleware:

async fn middleware(&self, ctx: &ExtensionContext<'_>, next: NextMiddleware<'_>) -> MiddlewareResult {
  // Logic to your middleware.

  /*
   * Final step to your middleware, we call the next function which will trigger
   * the execution of the next middleware. It's like a `callback` in JavaScript.
   */
  next.run(ctx).await
}

As you have seen, a Middleware is only a function calling the next function at the end, but we could also do a middleware with the next.run function at the start. This is where it's becoming tricky: depending on where you put your logic and where is the next.run call, your logic won't have the same execution order.

Depending on your logic code, you'll want to process it before or after the next.run call. If you need more information about middlewares, there are a lot of things on the web.

Processing of a query

There are several steps to go to process a query to completion, you'll be able to create extension based on these hooks.

request

First, when we receive a request, if it's not a subscription, the first function to be called will be request, it's the first step, it's the function called at the incoming request, and it's also the function which will output the response to the user.

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

Depending on where you put your logic code, it'll be executed at the beginning or at the end of the query being processed.

#![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 {
    // The code here will be run before the prepare_request is executed.
    let result = next.run(ctx).await;
    // The code after the completion of this future will be after the processing, just before sending the result to the user.
    result
}
}
}

prepare_request

Just after the request, we will have the prepare_request lifecycle, which will be hooked.

#![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> {
    // The code here will be un before the prepare_request is executed, just after the request lifecycle hook.
    let result = next.run(ctx, request).await;
    // The code here will be run just after the prepare_request
    result
}
}
}

parse_query

The parse_query will create a GraphQL ExecutableDocument on your query, it'll check if the query is valid for the GraphQL Spec. Usually the implemented spec in async-graphql tends to be the last stable one (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

The validation step will check (depending on your validation_mode) rules the query should abide to and give the client data about why the query is not valid.

#![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
}
}
}

execute

The execution step is a huge one, it'll start the execution of the query by calling each resolver concurrently for a Query and serially for a 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 {
    // Before starting resolving the whole query
    let result = next.run(ctx, operation_name).await;
    // After resolving the whole query
    result
}
}
}

resolve

The resolve step is launched for each field.

#![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>> {
    // Logic before resolving the field
    let result = next.run(ctx, info).await;
    // Logic after resolving the field
    result
}
}
}

subscribe

The subscribe lifecycle has the same behavior as the request but for a Subscritpion.

#![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)
}
}
}

Extensions available

There are a lot of available extensions in the async-graphql to empower your GraphQL Server, some of these documentations are documented here.

Analyzer

Available in the repository

The analyzer extension will output a field containing complexity and depth in the response extension field of each query.

Apollo Persisted Queries

Available in the repository

To improve network performance for large queries, you can enable this Persisted Queries extension. With this extension enabled, each unique query is associated to a unique identifier, so clients can send this identifier instead of the corresponding query string to reduce requests sizes.

This extension doesn't force you to use some cache strategy, you can choose the caching strategy you want, you'll just have to implement the 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 is an extension which includes analytics data for your queries. This extension works to follow the old and now deprecated Apollo Tracing Spec. If you want to check the newer Apollo Reporting Protocol, it's implemented by async-graphql Apollo studio extension for Apollo Studio.

Apollo Studio

Available at async-graphql/async_graphql_apollo_studio_extension

Apollo Studio is a cloud platform that helps you build, validate, and secure your organization's graph (description from the official documentation). It's a service allowing you to monitor & work with your team around your GraphQL Schema. async-graphql provides an extension implementing the official Apollo Specification available at async-graphql-extension-apollo-tracing and Crates.io.

Logger

Available in the repository

Logger is a simple extension allowing you to add some logging feature to async-graphql. It's also a good example to learn how to create your own extension.

OpenTelemetry

Available in the repository

OpenTelemetry is an extension providing an integration with the opentelemetry crate to allow your application to capture distributed traces and metrics from async-graphql.

Tracing

Available in the repository

Tracing is a simple extension allowing you to add some tracing feature to async-graphql. A little like the Logger extension.

Integrations

Async-graphql supports several common Rust web servers.

Even if the server you are currently using is not in the above list, it is quite simple to implement similar functionality yourself.

Poem

Request example

#![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));
}

Subscription example

#![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)));
}

More examples

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

Warp

For Async-graphql-warp, two Filter integrations are provided: graphql and graphql_subscription.

The graphql filter is used for execution Query and Mutation requests. It extracts GraphQL request and outputs async_graphql::Schema and async_graphql::Request. You can combine other filters later, or directly call Schema::execute to execute the query.

graphql_subscription is used to implement WebSocket subscriptions. It outputs warp::Reply.

Request example

#![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 {
    // Execute query
    let resp = schema.execute(request).await;

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

Subscription example

#![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;
}
}

More examples

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

Actix-web

Request example

When you define your actix_web::App you need to pass in the Schema as data.

#![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 now accessible here
    schema: web::Data<Schema<Query, EmptyMutation, EmptySubscription>>,
    request: GraphQLRequest,
) -> web::Json<GraphQLResponse> {
    web::Json(schema.execute(request.into_inner()).await.into())
}
}

Subscription example

#![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)
}
}

More examples

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

Advanced topics

Custom scalars

In Async-graphql most common scalar types are built in, but you can also create your own scalar types.

Using async-graphql::Scalar, you can add support for a scalar when you implement it. You only need to implement parsing and output functions.

The following example defines a 64-bit integer scalar where its input and output are strings.

#![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 {
            // Parse the integer value
            Ok(value.parse().map(StringNumber)?)
        } else {
            // If the type does not match
            Err(InputValueError::expected_type(value))
        }
    }

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

Use scalar! macro to define scalar

If your type implemented serde::Serialize and serde::Deserialize, then you can use this macro to define a scalar more simply.

#![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);

// Rename to `MV`.
// scalar!(MyValue, "MV");

// Rename to `MV` and add description.
// scalar!(MyValue, "MV", "This is my value");
}

Optimizing N+1 queries

Have you noticed some GraphQL queries end can make hundreds of database queries, often with mostly repeated data? Lets take a look why and how to fix it.

Query Resolution

Imagine if you have a simple query like this:

query {
  todos {
    users {
      name
    }
  }
}

and User resolver is like this:

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

The query executor will call the Todos resolver which does a select * from todo and return N todos. Then for each of the todos, concurrently, call the User resolver, SELECT from USER where id = todo.user_id.

eg:

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

After executing SELECT name FROM user WHERE id = $1 many times, and most Todo objects belong to the same user, we need to optimize these codes!

Dataloader

We need to group queries and exclude duplicate queries. Dataloader can do this. facebook gives a request-scope batch and caching solution.

The following is a simplified example of using DataLoader to optimize queries, there is also a full code example available in GitHub.

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

struct UserNameLoader {
    pool: sqlx::PgPool,
}

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> {
        Ok(sqlx::query_as("SELECT name FROM user WHERE id = ANY($1)")
            .bind(keys)
            .fetch(&self.pool)
            .map_ok(|name: String| name)
            .map_err(Arc::new)
            .try_collect().await?)
    }
}

#[derive(SimpleObject)]
#[graphql(complex)]
struct User {
    id: u64,
}

#[ComplexObject]
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())
    }
}

To expose UserNameLoader in the ctx, you have to register it with the schema, along with a task spawner, e.g. async_std::task::spawn:

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

In the end, only two SQLs are needed to query the results we want!

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

Implement multiple data types

You can implement multiple data types for the same Loader, like this:

extern crate async_graphql;
use async_graphql::*;
struct PostgresLoader {
    pool: sqlx::PgPool,
}

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> {
        // Load users from database
    }
}

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> {
        // Load todos from database
    }
}

Custom directive

There are two types of directives in GraphQL: executable and type system. Executable directives are used by the client within an operation to modify the behavior (like the built-in @include and @skip directives). Type system directives provide additional information about the types, potentially modifying how the server behaves (like @deprecated and @oneOf). async-graphql allows you to declare both types of custom directives, with different limitations on each.

Executable directives

To create a custom executable directive, you need to implement the CustomDirective trait, and then use the Directive macro to generate a factory function that receives the parameters of the directive and returns an instance of the directive.

Currently async-graphql only supports custom executable directives located at 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 }
}
}

Register the directive when building the schema:

#![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();
}

Type system directives

To create a custom type system directive, you can use the #[TypeDirective] macro on a function:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[TypeDirective(
    location = "FieldDefinition",
    location = "Object",
)]
fn testDirective(scope: String, input: u32, opt: Option<u64>) {}
}

Current only the FieldDefinition and Object locations are supported, you can select one or both. After declaring the directive, you can apply it to a relevant location (after importing the function) like this:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[TypeDirective(
location = "FieldDefinition",
location = "Object",
)]
fn testDirective(scope: String, input: u32, opt: Option<u64>) {}
#[derive(SimpleObject)]
#[graphql(
    directive = testDirective::apply("simple object type".to_string(), 1, Some(3))
)]
struct SimpleValue {
    #[graphql(
        directive = testDirective::apply("field and param with \" symbol".to_string(), 2, Some(3))
    )]
    some_data: String,
}
}

This example produces a schema like this:

type SimpleValue @testDirective(scope: "simple object type", input: 1, opt: 3) {
	someData: String! @testDirective(scope: "field and param with \" symbol", input: 2, opt: 3)
}

directive @testDirective(scope: String!, input: Int!, opt: Int) on FIELD_DEFINITION | OBJECT

Note: To use a type-system directive with Apollo Federation's @composeDirective, see the federation docs

Apollo Federation

Apollo Federation is a GraphQL architecture for combining multiple GraphQL services, or subgraphs, into a single supergraph. You can read more in the official documentation.

To see a complete example of federation, check out the federation example.

Enabling federation support

async-graphql supports all the functionality of Apollo Federation v2. Support will be enabled automatically if any #[graphql(entity)] resolvers are found in the schema. To enable it manually, use the enable_federation method on the SchemaBuilder.

extern crate async_graphql;
use async_graphql::*;
struct Query;
#[Object]
impl Query {
   async fn hello(&self) -> String { "Hello".to_string() }
}
fn main() {
  let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
    .enable_federation()
    .finish();
  // ... Start your server of choice
}

This will define the @link directive on your schema to enable Federation v2.

Entities and @key

Entities are a core feature of federation, they allow multiple subgraphs to contribute fields to the same type. An entity is a GraphQL type with at least one @key directive. To create a @key for a type, create a reference resolver using the #[graphql(entity)] attribute. This resolver should be defined on the Query struct, but will not appear as a field in the schema.

Even though a reference resolver looks up an individual entity, it is crucial that you use a dataloader in the implementation. The federation router will look up entities in batches, which can quickly lead the N+1 performance issues.

Example

#![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 }
    }
}
}

Notice the difference between these three lookup functions, which are all looking for the User object.

  • find_user_by_id: Use id to find a User object, the key for User is id.

  • find_user_by_id_with_username: Use id to find an User object, the key for User is id, and the username field value of the User object is requested (e.g., via @external and @requires).

  • find_user_by_id_and_username: Use id and username to find an User object, the keys for User are id and username.

The resulting schema will look like this:

type Query {
  # These fields will not be exposed to users, they are only used by the router to resolve entities
  _entities(representations: [_Any!]!): [_Entity]!
  _service: _Service!
}

type User @key(fields: "id") @key(fields: "id username") {
  id: ID!
}

Defining a compound primary key

A single primary key can consist of multiple fields, and even nested fields, you can use InputObject to implements a nested primary key.

In the following example, the primary key of the User object is key { a b }.

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct User { key: Key }
#[derive(SimpleObject)]
struct Key { a: i32, b: 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 {
    let NestedKey { a, b } = key;
    User { key: Key{a, b} }
  }
}
}

The resulting schema will look like this:

type Query {
  # These fields will not be exposed to users, they are only used by the router to resolve entities
  _entities(representations: [_Any!]!): [_Entity]!
  _service: _Service!
}

type User @key(fields: "key { a b }") {
  key: Key!
}

type Key {
  a: Int!
  b: Int!
}

Creating unresolvable entities

There are certain times when you need to reference an entity, but not add any fields to it. This is particularly useful when you want to link data from separate subgraphs together, but neither subgraph has all the data.

If you wanted to implement the products and reviews subgraphs example from the Apollo Docs, you would create the following types for the reviews subgraph:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct Review {
    product: Product,
    score: u64,
}

#[derive(SimpleObject)]
#[graphql(unresolvable)]
struct Product {
    id: u64,
}
}

This will add the @key(fields: "id", resolvable: false) directive to the Product type in the reviews subgraph.

For more complex entity keys, such as ones with nested fields in compound keys, you can override the fields in the directive as so:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
#[graphql(unresolvable = "id organization { id }")]
struct User {
    id: u64,
    organization: Organization,
}

#[derive(SimpleObject)]
struct Organization {
    id: u64,
}
}

However, it is important to note that no validation will be done to check that these fields exist.

@shareable

Apply the @shareable directive to a type or field to indicate that multiple subgraphs can resolve it.

@shareable fields

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
#[graphql(complex)]
struct Position {
  #[graphql(shareable)]
  x: u64,
}

#[ComplexObject]
impl Position {
  #[graphql(shareable)]
  async fn y(&self) -> u64 {
    0
  }
}
}

The resulting schema will look like this:

type Position {
  x: Int! @shareable
  y: Int! @shareable
}

@shareable type

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
#[graphql(shareable)]
struct Position {
  x: u64,
  y: u64,
}
}

The resulting schema will look like this:

type Position @shareable {
  x: Int!
  y: Int!
}

@inaccessible

The @inaccessible directive is used to omit something from the supergraph schema (e.g., if it's not yet added to all subgraphs which share a @shareable type).

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
#[graphql(shareable)]
struct Position {
  x: u32,
  y: u32,
  #[graphql(inaccessible)]
  z: u32,
} 
}

Results in:

type Position @shareable {
  x: Int!
  y: Int!
  z: Int! @inaccessible
}

@override

The @override directive is used to take ownership of a field from another subgraph. This is useful for migrating a field from one subgraph to another.

For example, if you add a new "Inventory" subgraph which should take over responsibility for the inStock field currently provided by the "Products" subgraph, you might have something like this:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct Product {
  id: ID,
  #[graphql(override_from = "Products")]
  in_stock: bool,
}
}

Which results in:

type Product @key(fields: "id") {
  id: ID!
  inStock: Boolean! @override(from: "Products")
}

@external

The @external directive is used to indicate that a field is usually provided by another subgraph, but is sometimes required by this subgraph (when combined with @requires) or provided by this subgraph (when combined with @provides).

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct Product {
  id: ID,
  #[graphql(external)]
  name: String,
  in_stock: bool,
}
}

Results in:

type Product {
  id: ID!
  name: String! @external
  inStock: Boolean!
}

@provides

The @provides directive is used to indicate that a field is provided by this subgraph, but only sometimes.

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct Product {
    id: ID,
    #[graphql(external)]
    human_name: String,
    in_stock: bool,
}

struct Query;

#[Object]
impl Query {
    /// This operation will provide the `humanName` field on `Product
    #[graphql(provides = "humanName")]
    async fn out_of_stock_products(&self) -> Vec<Product> {
      vec![Product {
        id: "1".into(),
        human_name: "My Product".to_string(),
        in_stock: false,
      }]
    }
    async fn discontinued_products(&self) -> Vec<Product> {
        vec![Product {
            id: "2".into(),
            human_name: String::new(),  // This is ignored by the router
            in_stock: false,
        }]
    }
    #[graphql(entity)]
    async fn find_product_by_id(&self, id: ID) -> Product {
        Product {
            id,
            human_name: String::new(),  // This is ignored by the router
            in_stock: true,
        }
    }
}
}

Note that the #[graphql(provides)] attribute takes the field name as it appears in the schema, not the Rust field name.

The resulting schema will look like this:

type Product @key(fields: "id") {
    id: ID!
    humanName: String! @external
    inStock: Boolean!
}

type Query {
    outOfStockProducts: [Product!]! @provides(fields: "humanName")
    discontinuedProducts: [Product!]!
}

@requires

The @requires directive is used to indicate that an @external field is required for this subgraph to resolve some other field(s). If our shippingEstimate field requires the size and weightInPounts fields, then we might want a subgraph entity which looks like this:

type Product @key(fields: "id") {
  id: ID!
  size: Int! @external
  weightInPounds: Int! @external
  shippingEstimate: String! @requires(fields: "size weightInPounds")
}

In order to implement this in Rust, we can use the #[graphql(requires)] attribute:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
#[graphql(complex)]
struct Product {
  id: ID,
  #[graphql(external)]
  size: u32,
  #[graphql(external)]
  weight_in_pounds: u32,
}

#[ComplexObject]
impl Product {
  #[graphql(requires = "size weightInPounds")]
  async fn shipping_estimate(&self) -> String {
    let price = self.size * self.weight_in_pounds;
    format!("${}", price)
  }
}
}

Note that we use the GraphQL field name weightInPounds, not the Rust field name weight_in_pounds in requires. To populate those external fields, we add them as arguments in the entity resolver:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
struct Product {
    id: ID,
    #[graphql(external)]
    size: u32,
    #[graphql(external)]
    weight_in_pounds: u32,
}
struct Query;
#[Object]
impl Query {
  #[graphql(entity)]
  async fn find_product_by_id(
    &self, 
    #[graphql(key)] id: ID, 
    size: Option<u32>, 
    weight_in_pounds: Option<u32>
  ) -> Product {
    Product {
      id,
      size: size.unwrap_or_default(),
      weight_in_pounds: weight_in_pounds.unwrap_or_default(),
    }
  }
}
}

The inputs are Option<> even though the fields are required. This is because the external fields are only passed to the subgraph when the field(s) that require them are being selected. If the shippingEstimate field is not selected, then the size and weightInPounds fields will not be passed to the subgraph. Always use optional types for external fields.

We have to put something in place for size and weight_in_pounds as they are still required fields on the type, so we use unwrap_or_default() to provide a default value. This looks a little funny, as we're populating the fields with nonsense values, but we have confidence that they will not be needed if they were not provided. Make sure to use @requires if you are consuming @external fields, or your code will be wrong.

Nested @requires

A case where the @requires directive can be confusing is when there are nested entities. For example, if we had an Order type which contained a Product, then we would need an entity resolver like this:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
pub struct Order { id: ID }
struct Query;
#[Object]
impl Query {
  #[graphql(entity)]
  async fn find_order_by_id(&self, id: ID) -> Option<Order> {
      Some(Order { id })
  }
}
}

There are no inputs on this entity resolver, so how do we populate the size and weight_in_pounds fields on Product if a user has a query like order { product { shippingEstimate } }? The supergraph implementation will solve this for us by calling the find_product_by_id separately for any fields which have a @requires directive, so the subgraph code does not need to worry about how entities relate.

@tag

The @tag directive is used to add metadata to a schema location for features like contracts. To add a tag like this:

type User @tag(name: "team-accounts") {
  id: String!
  name: String!
}

You can write code like this:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[derive(SimpleObject)]
#[graphql(tag = "team-accounts")]
struct User {
  id: ID,
  name: String,
}
}

@composeDirective

The @composeDirective directive is used to add a custom type system directive to the supergraph schema. Without @composeDirective, and custom type system directives are omitted from the composed supergraph schema. To include a custom type system directive as a composed directive, just add the composable attribute to the #[TypeDirective] macro:

#![allow(unused)]
fn main() {
extern crate async_graphql;
use async_graphql::*;
#[TypeDirective(
    location = "Object",
    composable = "https://custom.spec.dev/extension/v1.0",
)]
fn custom() {}
}

In addition to the normal type system directive behavior, this will add the following bits to the output schema:

extend schema @link(
	url: "https://custom.spec.dev/extension/v1.0"
	import: ["@custom"]
)
	@composeDirective(name: "@custom")