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 SimpleObject
s
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 Object
s 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
andmethod
exist together,name
is the GraphQL field name and themethod
is the resolver function name. - When only
name
exists,name.to_camel_case()
is the GraphQL field name and thename
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 InputObject
s
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 Error
s 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.
- Poem async-graphql-poem
- Actix-web async-graphql-actix-web
- Warp async-graphql-warp
- Axum async-graphql-axum
- Rocket async-graphql-rocket
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
: Useid
to find aUser
object, the key forUser
isid
. -
find_user_by_id_with_username
: Useid
to find anUser
object, the key forUser
isid
, and theusername
field value of theUser
object is requested (e.g., via@external
and@requires
). -
find_user_by_id_and_username
: Useid
andusername
to find anUser
object, the keys forUser
areid
andusername
.
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")