From e949cb441c3a1b7e1f1deec9be99d0c104edad9f Mon Sep 17 00:00:00 2001 From: sunli Date: Wed, 20 May 2020 08:18:28 +0800 Subject: [PATCH] Add defer tests --- async-graphql-derive/src/enum.rs | 2 +- async-graphql-derive/src/interface.rs | 4 +- async-graphql-derive/src/object.rs | 6 +- async-graphql-derive/src/scalar.rs | 2 +- async-graphql-derive/src/simple_object.rs | 4 +- async-graphql-derive/src/subscription.rs | 8 +- async-graphql-derive/src/union.rs | 2 +- src/base.rs | 44 +++++++---- src/context.rs | 19 +++-- src/error.rs | 8 +- src/extensions/apollo_tracing.rs | 2 +- src/http/into_query_builder.rs | 1 - src/http/mod.rs | 4 + src/lib.rs | 1 + src/query.rs | 92 ++++++++++++++--------- src/scalars/string.rs | 12 ++- src/schema.rs | 10 +++ src/types/connection/connection_type.rs | 19 +++-- src/types/connection/edge.rs | 13 +++- src/types/deferred.rs | 91 ++++++++++++++-------- src/types/empty_mutation.rs | 13 +++- src/types/empty_subscription.rs | 16 +--- src/types/list.rs | 21 ++++-- src/types/optional.rs | 14 ++-- src/types/query_root.rs | 19 +++-- tests/defer.rs | 76 ++++++++++++++++++- 26 files changed, 336 insertions(+), 167 deletions(-) diff --git a/async-graphql-derive/src/enum.rs b/async-graphql-derive/src/enum.rs index dcf4bb76..fd8c858c 100644 --- a/async-graphql-derive/src/enum.rs +++ b/async-graphql-derive/src/enum.rs @@ -163,7 +163,7 @@ pub fn generate(enum_args: &args::Enum, input: &DeriveInput) -> Result, _pos: #crate_name::Pos) -> #crate_name::Result<#crate_name::serde_json::Value> { + async fn resolve(&self, _: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::query::Field>) -> #crate_name::Result<#crate_name::serde_json::Value> { #crate_name::EnumType::resolve_enum(self) } } diff --git a/async-graphql-derive/src/interface.rs b/async-graphql-derive/src/interface.rs index 8d526275..5add07d8 100644 --- a/async-graphql-derive/src/interface.rs +++ b/async-graphql-derive/src/interface.rs @@ -245,7 +245,7 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result if ctx.name.node == #name { #(#get_params)* let ctx_obj = ctx.with_selection_set(&ctx.selection_set); - return #crate_name::OutputValueType::resolve(&#resolve_obj, &ctx_obj, ctx.position()).await; + return #crate_name::OutputValueType::resolve(&#resolve_obj, &ctx_obj, ctx.item).await; } }); } @@ -325,7 +325,7 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result #[#crate_name::async_trait::async_trait] impl #generics #crate_name::OutputValueType for #ident #generics { - async fn resolve(&self, ctx: &#crate_name::ContextSelectionSet<'_>, pos: #crate_name::Pos) -> #crate_name::Result<#crate_name::serde_json::Value> { + async fn resolve(&self, ctx: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::query::Field>) -> #crate_name::Result<#crate_name::serde_json::Value> { #crate_name::do_resolve(ctx, self).await } } diff --git a/async-graphql-derive/src/object.rs b/async-graphql-derive/src/object.rs index 2901debb..fc10e7af 100644 --- a/async-graphql-derive/src/object.rs +++ b/async-graphql-derive/src/object.rs @@ -166,7 +166,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< if let (#(#key_pat),*) = (#(#key_getter),*) { #guard let ctx_obj = ctx.with_selection_set(&ctx.selection_set); - return #crate_name::OutputValueType::resolve(&#do_find, &ctx_obj, ctx.position()).await; + return #crate_name::OutputValueType::resolve(&#do_find, &ctx_obj, ctx.item).await; } } }, @@ -391,7 +391,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< #(#get_params)* #guard let ctx_obj = ctx.with_selection_set(&ctx.selection_set); - return OutputValueType::resolve(&#resolve_obj, &ctx_obj, ctx.position()).await; + return OutputValueType::resolve(&#resolve_obj, &ctx_obj, ctx.item).await; } }); @@ -482,7 +482,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< #[#crate_name::async_trait::async_trait] impl #generics #crate_name::OutputValueType for #self_ty #where_clause { - async fn resolve(&self, ctx: &#crate_name::ContextSelectionSet<'_>, pos: #crate_name::Pos) -> #crate_name::Result<#crate_name::serde_json::Value> { + async fn resolve(&self, ctx: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::query::Field>) -> #crate_name::Result<#crate_name::serde_json::Value> { #crate_name::do_resolve(ctx, self).await } } diff --git a/async-graphql-derive/src/scalar.rs b/async-graphql-derive/src/scalar.rs index 6856a083..2bd62d34 100644 --- a/async-graphql-derive/src/scalar.rs +++ b/async-graphql-derive/src/scalar.rs @@ -58,7 +58,7 @@ pub fn generate(scalar_args: &args::Scalar, item_impl: &mut ItemImpl) -> Result< async fn resolve( &self, _: &#crate_name::ContextSelectionSet<'_>, - _pos: #crate_name::Pos, + _field: &#crate_name::Positioned<#crate_name::parser::query::Field> ) -> #crate_name::Result<#crate_name::serde_json::Value> { self.to_json() } diff --git a/async-graphql-derive/src/simple_object.rs b/async-graphql-derive/src/simple_object.rs index aac22320..1cac5df0 100644 --- a/async-graphql-derive/src/simple_object.rs +++ b/async-graphql-derive/src/simple_object.rs @@ -117,7 +117,7 @@ pub fn generate(object_args: &args::Object, input: &mut DeriveInput) -> Result Result, _pos: #crate_name::Pos) -> #crate_name::Result<#crate_name::serde_json::Value> { + async fn resolve(&self, ctx: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::query::Field>) -> #crate_name::Result<#crate_name::serde_json::Value> { #crate_name::do_resolve(ctx, self).await } } diff --git a/async-graphql-derive/src/subscription.rs b/async-graphql-derive/src/subscription.rs index dd50dd38..13d6661f 100644 --- a/async-graphql-derive/src/subscription.rs +++ b/async-graphql-derive/src/subscription.rs @@ -242,7 +242,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< #(#get_params)* #guard let field_name = std::sync::Arc::new(ctx.result_name().to_string()); - let field_selection_set = std::sync::Arc::new(ctx.selection_set.clone()); + let field = std::sync::Arc::new(ctx.item.clone()); let pos = ctx.position(); let schema_env = schema_env.clone(); @@ -252,7 +252,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< move |msg| { let schema_env = schema_env.clone(); let query_env = query_env.clone(); - let field_selection_set = field_selection_set.clone(); + let field = field.clone(); let field_name = field_name.clone(); async move { let resolve_id = std::sync::atomic::AtomicUsize::default(); @@ -262,11 +262,11 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< parent: None, segment: #crate_name::QueryPathSegment::Name(&field_name), }), - &*field_selection_set, + &field.selection_set, &resolve_id, None, ); - #crate_name::OutputValueType::resolve(&msg, &ctx_selection_set, pos).await + #crate_name::OutputValueType::resolve(&msg, &ctx_selection_set, &*field).await } } }) diff --git a/async-graphql-derive/src/union.rs b/async-graphql-derive/src/union.rs index 096fde5c..23462ac5 100644 --- a/async-graphql-derive/src/union.rs +++ b/async-graphql-derive/src/union.rs @@ -150,7 +150,7 @@ pub fn generate(union_args: &args::Interface, input: &DeriveInput) -> Result, pos: #crate_name::Pos) -> #crate_name::Result<#crate_name::serde_json::Value> { + async fn resolve(&self, ctx: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::query::Field>) -> #crate_name::Result<#crate_name::serde_json::Value> { #crate_name::do_resolve(ctx, self).await } } diff --git a/src/base.rs b/src/base.rs index 164f9ac7..19361242 100644 --- a/src/base.rs +++ b/src/base.rs @@ -1,9 +1,9 @@ -use crate::parser::Pos; use crate::registry::Registry; use crate::{ - registry, Context, ContextSelectionSet, FieldResult, InputValueResult, QueryError, Result, - Value, ID, + registry, Context, ContextSelectionSet, FieldResult, InputValueResult, Positioned, QueryError, + Result, Value, ID, }; +use async_graphql_parser::query::Field; use std::borrow::Cow; use std::future::Future; use std::pin::Pin; @@ -59,7 +59,11 @@ pub trait InputValueType: Type + Sized { #[async_trait::async_trait] pub trait OutputValueType: Type { /// Resolve an output value to `serde_json::Value`. - async fn resolve(&self, ctx: &ContextSelectionSet<'_>, pos: Pos) -> Result; + async fn resolve( + &self, + ctx: &ContextSelectionSet<'_>, + field: &Positioned, + ) -> Result; } #[allow(missing_docs)] @@ -166,8 +170,12 @@ impl Type for &T { #[async_trait::async_trait] impl OutputValueType for &T { #[allow(clippy::trivially_copy_pass_by_ref)] - async fn resolve(&self, ctx: &ContextSelectionSet<'_>, pos: Pos) -> Result { - T::resolve(*self, ctx, pos).await + async fn resolve( + &self, + ctx: &ContextSelectionSet<'_>, + field: &Positioned, + ) -> Result { + T::resolve(*self, ctx, field).await } } @@ -185,8 +193,12 @@ impl Type for Box { impl OutputValueType for Box { #[allow(clippy::trivially_copy_pass_by_ref)] #[allow(clippy::borrowed_box)] - async fn resolve(&self, ctx: &ContextSelectionSet<'_>, pos: Pos) -> Result { - T::resolve(&*self, ctx, pos).await + async fn resolve( + &self, + ctx: &ContextSelectionSet<'_>, + field: &Positioned, + ) -> Result { + T::resolve(&*self, ctx, field).await } } @@ -203,8 +215,12 @@ impl Type for Arc { #[async_trait::async_trait] impl OutputValueType for Arc { #[allow(clippy::trivially_copy_pass_by_ref)] - async fn resolve(&self, ctx: &ContextSelectionSet<'_>, pos: Pos) -> Result { - T::resolve(&*self, ctx, pos).await + async fn resolve( + &self, + ctx: &ContextSelectionSet<'_>, + field: &Positioned, + ) -> Result { + T::resolve(&*self, ctx, field).await } } @@ -227,15 +243,15 @@ impl OutputValueType for FieldResult { async fn resolve( &self, ctx: &ContextSelectionSet<'_>, - pos: Pos, + field: &Positioned, ) -> crate::Result { match self { - Ok(value) => Ok(OutputValueType::resolve(value, ctx, pos).await?), + Ok(value) => Ok(OutputValueType::resolve(value, ctx, field).await?), Err(err) => Err(err.clone().into_error_with_path( - pos, + field.position(), match &ctx.path_node { Some(path) => path.to_json(), - None => serde_json::Value::Null, + None => Vec::new(), }, )), } diff --git a/src/context.rs b/src/context.rs index bfc2c096..2510684d 100644 --- a/src/context.rs +++ b/src/context.rs @@ -196,7 +196,7 @@ impl<'a> QueryPathNode<'a> { } #[doc(hidden)] - pub fn to_json(&self) -> serde_json::Value { + pub fn to_json(&self) -> Vec { let mut path: Vec = Vec::new(); self.for_each(|segment| { path.push(match segment { @@ -204,7 +204,7 @@ impl<'a> QueryPathNode<'a> { QueryPathSegment::Name(name) => (*name).to_string().into(), }) }); - path.into() + path } } @@ -242,19 +242,17 @@ pub type BoxDeferFuture = Pin> + Send + 'static>>; #[doc(hidden)] -#[derive(Default)] -pub struct DeferList(pub Mutex>); +pub struct DeferList { + pub path_prefix: Vec, + pub futures: Mutex>, +} impl DeferList { - pub(crate) fn into_inner(self) -> Vec { - self.0.into_inner() - } - pub(crate) fn append(&self, fut: F) where F: Future> + Send + 'static, { - self.0.lock().push(Box::pin(fut)); + self.futures.lock().push(Box::pin(fut)); } } @@ -266,7 +264,8 @@ pub struct ContextBase<'a, T> { pub(crate) resolve_id: ResolveId, pub(crate) inc_resolve_id: &'a AtomicUsize, pub(crate) extensions: &'a [BoxExtension], - pub(crate) item: T, + #[doc(hidden)] + pub item: T, pub(crate) schema_env: &'a SchemaEnv, pub(crate) query_env: &'a QueryEnv, pub(crate) defer_list: Option<&'a DeferList>, diff --git a/src/error.rs b/src/error.rs index 658ddcab..0e49eb4d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -59,10 +59,14 @@ impl FieldError { } #[doc(hidden)] - pub fn into_error_with_path(self, pos: Pos, path: serde_json::Value) -> Error { + pub fn into_error_with_path(self, pos: Pos, path: Vec) -> Error { Error::Query { pos, - path: Some(path), + path: if !path.is_empty() { + Some(path.into()) + } else { + None + }, err: QueryError::FieldError { err: self.0, extended_error: self.1, diff --git a/src/extensions/apollo_tracing.rs b/src/extensions/apollo_tracing.rs index 43a62b71..d77fd218 100644 --- a/src/extensions/apollo_tracing.rs +++ b/src/extensions/apollo_tracing.rs @@ -90,7 +90,7 @@ impl Extension for ApolloTracing { inner.pending_resolves.insert( info.resolve_id.current, PendingResolve { - path: info.path_node.to_json(), + path: info.path_node.to_json().into(), field_name: info.path_node.field_name().to_string(), parent_type: info.parent_type.to_string(), return_type: info.return_type.to_string(), diff --git a/src/http/into_query_builder.rs b/src/http/into_query_builder.rs index 609b4c8e..64570335 100644 --- a/src/http/into_query_builder.rs +++ b/src/http/into_query_builder.rs @@ -96,7 +96,6 @@ where let mut file = tempfile::tempfile().map_err(ParseRequestError::Io)?; while let Some(chunk) = field.chunk().await.unwrap() { - println!("{:?}", chunk); file.write(&chunk).map_err(ParseRequestError::Io)?; } file.seek(SeekFrom::Start(0))?; diff --git a/src/http/mod.rs b/src/http/mod.rs index a5d7bb2b..4e5aa56e 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -59,6 +59,10 @@ impl Serialize for GQLResponse { match &self.0 { Ok(res) => { let mut map = serializer.serialize_map(None)?; + if let Some(path) = &res.path { + map.serialize_key("path")?; + map.serialize_value(path)?; + } map.serialize_key("data")?; map.serialize_value(&res.data)?; if res.extensions.is_some() { diff --git a/src/lib.rs b/src/lib.rs index ead117fa..1ff18954 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,6 +75,7 @@ #![warn(missing_docs)] #![allow(clippy::needless_doctest_main)] #![allow(clippy::needless_lifetimes)] +#![recursion_limit = "256"] #[macro_use] extern crate thiserror; diff --git a/src/query.rs b/src/query.rs index d912ac66..eac14673 100644 --- a/src/query.rs +++ b/src/query.rs @@ -43,7 +43,7 @@ pub trait IntoQueryBuilder: Sized { #[derive(Debug)] pub struct QueryResponse { /// Path for subsequent response - pub path: Option, + pub path: Option>, /// Data of query result pub data: serde_json::Value, @@ -56,34 +56,42 @@ pub struct QueryResponse { } impl QueryResponse { - pub(crate) fn merge(&mut self, resp: QueryResponse) { - if let Some(serde_json::Value::Array(items)) = resp.path { - let mut p = &mut self.data; - for item in items { - match item { - serde_json::Value::String(name) => { - if let serde_json::Value::Object(obj) = p { - if let Some(next) = obj.get_mut(&name) { - p = next; - continue; - } - } - return; - } - serde_json::Value::Number(idx) => { - if let serde_json::Value::Array(array) = p { - if let Some(next) = array.get_mut(idx.as_i64().unwrap() as usize) { - p = next; - continue; - } - } - return; - } - _ => {} - } - } - *p = resp.data; + pub(crate) fn apply_path_prefix(mut self, mut prefix: Vec) -> Self { + if let Some(path) = &mut self.path { + prefix.extend(path.drain(..)); + *path = prefix; + } else { + self.path = Some(prefix); } + self + } + + pub(crate) fn merge(&mut self, resp: QueryResponse) { + let mut p = &mut self.data; + for item in resp.path.unwrap_or_default() { + match item { + serde_json::Value::String(name) => { + if let serde_json::Value::Object(obj) = p { + if let Some(next) = obj.get_mut(&name) { + p = next; + continue; + } + } + return; + } + serde_json::Value::Number(idx) => { + if let serde_json::Value::Array(array) = p { + if let Some(next) = array.get_mut(idx.as_i64().unwrap() as usize) { + p = next; + continue; + } + } + return; + } + _ => {} + } + } + *p = resp.data; } } @@ -162,19 +170,26 @@ impl QueryBuilder { let (first_resp, defer_list) = self.execute_first(&schema).await?; yield first_resp; - let mut current_defer_list = defer_list.into_inner(); + let mut current_defer_list = Vec::new(); + for fut in defer_list.futures.into_inner() { + current_defer_list.push((defer_list.path_prefix.clone(), fut)); + } loop { - let mut new_defer_list = Vec::new(); - for defer in current_defer_list { - let mut res = defer.await?; - new_defer_list.extend((res.1).into_inner()); - yield res.0; + let mut next_defer_list = Vec::new(); + for (path_prefix, defer) in current_defer_list { + let (res, mut defer_list) = defer.await?; + for fut in defer_list.futures.into_inner() { + let mut next_path_prefix = path_prefix.clone(); + next_path_prefix.extend(defer_list.path_prefix.clone()); + next_defer_list.push((next_path_prefix, fut)); + } + yield res.apply_path_prefix(path_prefix); } - if new_defer_list.is_empty() { + if next_defer_list.is_empty() { break; } - current_defer_list = new_defer_list; + current_defer_list = next_defer_list; } }; Box::pin(stream) @@ -250,7 +265,10 @@ impl QueryBuilder { document, Arc::new(self.ctx_data.unwrap_or_default()), ); - let defer_list = DeferList::default(); + let defer_list = DeferList { + path_prefix: Vec::new(), + futures: Default::default(), + }; let ctx = ContextBase { path_node: None, resolve_id: ResolveId::root(), diff --git a/src/scalars/string.rs b/src/scalars/string.rs index 60bc5df0..4b9ed093 100644 --- a/src/scalars/string.rs +++ b/src/scalars/string.rs @@ -1,9 +1,9 @@ -use crate::parser::Pos; use crate::{ - registry, ContextSelectionSet, InputValueError, InputValueResult, OutputValueType, Result, - ScalarType, Type, Value, + registry, ContextSelectionSet, InputValueError, InputValueResult, OutputValueType, Positioned, + Result, ScalarType, Type, Value, }; use async_graphql_derive::Scalar; +use async_graphql_parser::query::Field; use std::borrow::Cow; /// The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. @@ -40,7 +40,11 @@ impl<'a> Type for &'a str { #[async_trait::async_trait] impl<'a> OutputValueType for &'a str { - async fn resolve(&self, _: &ContextSelectionSet<'_>, _pos: Pos) -> Result { + async fn resolve( + &self, + _: &ContextSelectionSet<'_>, + _field: &Positioned, + ) -> Result { Ok((*self).into()) } } diff --git a/src/schema.rs b/src/schema.rs index 488e0d04..60eac1cc 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -283,6 +283,16 @@ where QueryBuilder::new(query_source).execute(self).await } + /// Execute the query without create the `QueryBuilder`, returns a stream, the first result being the query result, + /// followed by the incremental result. Only when there are `@defer` and `@stream` directives + /// in the query will there be subsequent incremental results. + pub async fn execute_stream( + &self, + query_source: &str, + ) -> impl Stream> { + QueryBuilder::new(query_source).execute_stream(self).await + } + /// Create subscription stream, typically called inside the `SubscriptionTransport::handle_request` method pub async fn create_subscription_stream( &self, diff --git a/src/types/connection/connection_type.rs b/src/types/connection/connection_type.rs index 3c332220..04338269 100644 --- a/src/types/connection/connection_type.rs +++ b/src/types/connection/connection_type.rs @@ -3,8 +3,9 @@ use crate::types::connection::edge::Edge; use crate::types::connection::page_info::PageInfo; use crate::{ do_resolve, registry, Context, ContextSelectionSet, EmptyEdgeFields, Error, ObjectType, - OutputValueType, Pos, QueryError, Result, Type, + OutputValueType, Positioned, QueryError, Result, Type, }; +use async_graphql_parser::query::Field; use indexmap::map::IndexMap; use inflector::Inflector; use itertools::Itertools; @@ -179,19 +180,17 @@ impl ObjectType async fn resolve_field(&self, ctx: &Context<'_>) -> Result { if ctx.name.node == "pageInfo" { let ctx_obj = ctx.with_selection_set(&ctx.selection_set); - return OutputValueType::resolve(self.page_info().await, &ctx_obj, ctx.position()) - .await; + return OutputValueType::resolve(self.page_info().await, &ctx_obj, ctx.item).await; } else if ctx.name.node == "edges" { let ctx_obj = ctx.with_selection_set(&ctx.selection_set); - return OutputValueType::resolve(&self.edges().await, &ctx_obj, ctx.position()).await; + return OutputValueType::resolve(&self.edges().await, &ctx_obj, ctx.item).await; } else if ctx.name.node == "totalCount" { let ctx_obj = ctx.with_selection_set(&ctx.selection_set); - return OutputValueType::resolve(&self.total_count().await, &ctx_obj, ctx.position()) - .await; + return OutputValueType::resolve(&self.total_count().await, &ctx_obj, ctx.item).await; } else if ctx.name.node == T::type_name().to_plural().to_camel_case() { let ctx_obj = ctx.with_selection_set(&ctx.selection_set); let items = self.nodes.iter().map(|(_, _, item)| item).collect_vec(); - return OutputValueType::resolve(&items, &ctx_obj, ctx.position()).await; + return OutputValueType::resolve(&items, &ctx_obj, ctx.item).await; } Err(Error::Query { @@ -209,7 +208,11 @@ impl ObjectType impl OutputValueType for Connection { - async fn resolve(&self, ctx: &ContextSelectionSet<'_>, _pos: Pos) -> Result { + async fn resolve( + &self, + ctx: &ContextSelectionSet<'_>, + _field: &Positioned, + ) -> Result { do_resolve(ctx, self).await } } diff --git a/src/types/connection/edge.rs b/src/types/connection/edge.rs index 7ed81ea1..c4a81542 100644 --- a/src/types/connection/edge.rs +++ b/src/types/connection/edge.rs @@ -1,7 +1,8 @@ use crate::{ - do_resolve, registry, Context, ContextSelectionSet, ObjectType, OutputValueType, Pos, Result, - Type, + do_resolve, registry, Context, ContextSelectionSet, ObjectType, OutputValueType, Positioned, + Result, Type, }; +use async_graphql_parser::query::Field; use indexmap::map::IndexMap; use std::borrow::Cow; @@ -105,7 +106,7 @@ where async fn resolve_field(&self, ctx: &Context<'_>) -> Result { if ctx.name.node == "node" { let ctx_obj = ctx.with_selection_set(&ctx.selection_set); - return OutputValueType::resolve(self.node().await, &ctx_obj, ctx.position()).await; + return OutputValueType::resolve(self.node().await, &ctx_obj, ctx.item).await; } else if ctx.name.node == "cursor" { return Ok(self.cursor().await.into()); } @@ -120,7 +121,11 @@ where T: OutputValueType + Send + Sync + 'a, E: ObjectType + Sync + Send + 'a, { - async fn resolve(&self, ctx: &ContextSelectionSet<'_>, _pos: Pos) -> Result { + async fn resolve( + &self, + ctx: &ContextSelectionSet<'_>, + _field: &Positioned, + ) -> Result { do_resolve(ctx, self).await } } diff --git a/src/types/deferred.rs b/src/types/deferred.rs index f52f3e7b..83eedd03 100644 --- a/src/types/deferred.rs +++ b/src/types/deferred.rs @@ -1,9 +1,14 @@ use crate::context::DeferList; use crate::registry::Registry; -use crate::{ContextSelectionSet, OutputValueType, Pos, QueryResponse, Result, Type}; +use crate::{ContextSelectionSet, OutputValueType, Positioned, QueryResponse, Result, Type}; +use async_graphql_parser::query::Field; +use itertools::Itertools; use std::borrow::Cow; use std::sync::atomic::AtomicUsize; +/// Deferred type +/// +/// Allows to defer the type of results returned, Only takes effect when the @defer directive exists on the field. pub struct Deferred(T); impl From for Deferred { @@ -24,38 +29,62 @@ impl Type for Deferred { #[async_trait::async_trait] impl OutputValueType for Deferred { - async fn resolve(&self, ctx: &ContextSelectionSet<'_>, pos: Pos) -> Result { + async fn resolve( + &self, + ctx: &ContextSelectionSet<'_>, + field: &Positioned, + ) -> Result { if let Some(defer_list) = ctx.defer_list { - let obj = self.0.clone(); - let schema_env = ctx.schema_env.clone(); - let query_env = ctx.query_env.clone(); - let field_selection_set = ctx.item.clone(); - let path = ctx.path_node.as_ref().map(|path| path.to_json()); - defer_list.append(async move { - let inc_resolve_id = AtomicUsize::default(); - let defer_list = DeferList::default(); - let ctx = query_env.create_context( - &schema_env, - None, - &field_selection_set, - &inc_resolve_id, - Some(&defer_list), - ); - let data = obj.resolve(&ctx, pos).await?; + if ctx.is_defer(&field.directives) { + let obj = self.0.clone(); + let schema_env = ctx.schema_env.clone(); + let query_env = ctx.query_env.clone(); + let mut field = field.clone(); - Ok(( - QueryResponse { - path, - data, - extensions: None, - cache_control: Default::default(), - }, - defer_list, - )) - }); - Ok(serde_json::Value::Null) - } else { - OutputValueType::resolve(&self.0, ctx, pos).await + // remove @defer directive + if let Some((idx, _)) = field + .node + .directives + .iter() + .find_position(|d| d.name.as_str() == "defer") + { + field.node.directives.remove(idx); + } + + let path_prefix = ctx + .path_node + .as_ref() + .map(|path| path.to_json()) + .unwrap_or_default(); + + defer_list.append(async move { + let inc_resolve_id = AtomicUsize::default(); + let defer_list = DeferList { + path_prefix: path_prefix.clone(), + futures: Default::default(), + }; + let ctx = query_env.create_context( + &schema_env, + None, + &field.selection_set, + &inc_resolve_id, + Some(&defer_list), + ); + let data = obj.resolve(&ctx, &field).await?; + + Ok(( + QueryResponse { + path: Some(path_prefix), + data, + extensions: None, + cache_control: Default::default(), + }, + defer_list, + )) + }); + return Ok(serde_json::Value::Null); + } } + OutputValueType::resolve(&self.0, ctx, field).await } } diff --git a/src/types/empty_mutation.rs b/src/types/empty_mutation.rs index f32ddab6..846d2ddb 100644 --- a/src/types/empty_mutation.rs +++ b/src/types/empty_mutation.rs @@ -1,7 +1,8 @@ use crate::{ - registry, Context, ContextSelectionSet, Error, ObjectType, OutputValueType, Pos, QueryError, - Result, Type, + registry, Context, ContextSelectionSet, Error, ObjectType, OutputValueType, Positioned, + QueryError, Result, Type, }; +use async_graphql_parser::query::Field; use std::borrow::Cow; /// Empty mutation @@ -54,9 +55,13 @@ impl ObjectType for EmptyMutation { #[async_trait::async_trait] impl OutputValueType for EmptyMutation { - async fn resolve(&self, _ctx: &ContextSelectionSet<'_>, pos: Pos) -> Result { + async fn resolve( + &self, + _ctx: &ContextSelectionSet<'_>, + field: &Positioned, + ) -> Result { Err(Error::Query { - pos, + pos: field.position(), path: None, err: QueryError::NotConfiguredMutations, }) diff --git a/src/types/empty_subscription.rs b/src/types/empty_subscription.rs index cbd47e85..14c42374 100644 --- a/src/types/empty_subscription.rs +++ b/src/types/empty_subscription.rs @@ -1,8 +1,5 @@ use crate::context::QueryEnv; -use crate::{ - registry, Context, ContextSelectionSet, Error, OutputValueType, Pos, QueryError, Result, - SchemaEnv, SubscriptionType, Type, -}; +use crate::{registry, Context, Error, Pos, QueryError, Result, SchemaEnv, SubscriptionType, Type}; use futures::Stream; use std::borrow::Cow; use std::pin::Pin; @@ -52,14 +49,3 @@ impl SubscriptionType for EmptySubscription { }) } } - -#[async_trait::async_trait] -impl OutputValueType for EmptySubscription { - async fn resolve(&self, _ctx: &ContextSelectionSet<'_>, pos: Pos) -> Result { - Err(Error::Query { - pos, - path: None, - err: QueryError::NotConfiguredSubscriptions, - }) - } -} diff --git a/src/types/list.rs b/src/types/list.rs index dfbd1fb7..bcda0fe4 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -1,7 +1,8 @@ use crate::{ - registry, ContextSelectionSet, InputValueResult, InputValueType, OutputValueType, Pos, Result, - Type, Value, + registry, ContextSelectionSet, InputValueResult, InputValueType, OutputValueType, Positioned, + Result, Type, Value, }; +use async_graphql_parser::query::Field; use std::borrow::Cow; impl Type for Vec { @@ -37,11 +38,15 @@ impl InputValueType for Vec { #[allow(clippy::ptr_arg)] #[async_trait::async_trait] impl OutputValueType for Vec { - async fn resolve(&self, ctx: &ContextSelectionSet<'_>, pos: Pos) -> Result { + async fn resolve( + &self, + ctx: &ContextSelectionSet<'_>, + field: &Positioned, + ) -> Result { let mut futures = Vec::with_capacity(self.len()); for (idx, item) in self.iter().enumerate() { let ctx_idx = ctx.with_index(idx); - futures.push(async move { OutputValueType::resolve(item, &ctx_idx, pos).await }); + futures.push(async move { OutputValueType::resolve(item, &ctx_idx, field).await }); } Ok(futures::future::try_join_all(futures).await?.into()) } @@ -59,11 +64,15 @@ impl Type for &[T] { #[async_trait::async_trait] impl OutputValueType for &[T] { - async fn resolve(&self, ctx: &ContextSelectionSet<'_>, pos: Pos) -> Result { + async fn resolve( + &self, + ctx: &ContextSelectionSet<'_>, + field: &Positioned, + ) -> Result { let mut futures = Vec::with_capacity(self.len()); for (idx, item) in (*self).iter().enumerate() { let ctx_idx = ctx.with_index(idx); - futures.push(async move { OutputValueType::resolve(item, &ctx_idx, pos).await }); + futures.push(async move { OutputValueType::resolve(item, &ctx_idx, field).await }); } Ok(futures::future::try_join_all(futures).await?.into()) } diff --git a/src/types/optional.rs b/src/types/optional.rs index 3ddaa177..e93e8998 100644 --- a/src/types/optional.rs +++ b/src/types/optional.rs @@ -1,7 +1,8 @@ use crate::{ - registry, ContextSelectionSet, InputValueResult, InputValueType, OutputValueType, Pos, Result, - Type, Value, + registry, ContextSelectionSet, InputValueResult, InputValueType, OutputValueType, Positioned, + Result, Type, Value, }; +use async_graphql_parser::query::Field; use std::borrow::Cow; impl Type for Option { @@ -30,10 +31,13 @@ impl InputValueType for Option { #[async_trait::async_trait] impl OutputValueType for Option { - async fn resolve(&self, ctx: &ContextSelectionSet<'_>, pos: Pos) -> Result where - { + async fn resolve( + &self, + ctx: &ContextSelectionSet<'_>, + field: &Positioned, + ) -> Result where { if let Some(inner) = self { - OutputValueType::resolve(inner, ctx, pos).await + OutputValueType::resolve(inner, ctx, field).await } else { Ok(serde_json::Value::Null) } diff --git a/src/types/query_root.rs b/src/types/query_root.rs index b4889796..e17392fc 100644 --- a/src/types/query_root.rs +++ b/src/types/query_root.rs @@ -1,10 +1,11 @@ use crate::model::{__Schema, __Type}; use crate::scalars::Any; use crate::{ - do_resolve, registry, Context, ContextSelectionSet, Error, ObjectType, OutputValueType, Pos, - QueryError, Result, Type, Value, + do_resolve, registry, Context, ContextSelectionSet, Error, ObjectType, OutputValueType, + Positioned, QueryError, Result, Type, Value, }; use async_graphql_derive::SimpleObject; +use async_graphql_parser::query::Field; use indexmap::map::IndexMap; use std::borrow::Cow; @@ -84,7 +85,7 @@ impl ObjectType for QueryRoot { if self.disable_introspection { return Err(Error::Query { pos: ctx.position(), - path: Some(ctx.path_node.as_ref().unwrap().to_json()), + path: Some(ctx.path_node.as_ref().unwrap().to_json().into()), err: QueryError::FieldNotFound { field_name: ctx.name.to_string(), object: Self::type_name().to_string(), @@ -98,7 +99,7 @@ impl ObjectType for QueryRoot { registry: &ctx.schema_env.registry, }, &ctx_obj, - ctx.position(), + ctx.item, ) .await; } else if ctx.name.node == "__type" { @@ -111,7 +112,7 @@ impl ObjectType for QueryRoot { .get(&type_name) .map(|ty| __Type::new_simple(&ctx.schema_env.registry, ty)), &ctx_obj, - ctx.position(), + ctx.item, ) .await; } else if ctx.name.node == "_entities" { @@ -128,7 +129,7 @@ impl ObjectType for QueryRoot { sdl: Some(ctx.schema_env.registry.create_federation_sdl()), }, &ctx_obj, - ctx.position(), + ctx.item, ) .await; } @@ -139,7 +140,11 @@ impl ObjectType for QueryRoot { #[async_trait::async_trait] impl OutputValueType for QueryRoot { - async fn resolve(&self, ctx: &ContextSelectionSet<'_>, _pos: Pos) -> Result { + async fn resolve( + &self, + ctx: &ContextSelectionSet<'_>, + _field: &Positioned, + ) -> Result { do_resolve(ctx, self).await } } diff --git a/tests/defer.rs b/tests/defer.rs index 7e9ba9f0..6618076c 100644 --- a/tests/defer.rs +++ b/tests/defer.rs @@ -1,21 +1,38 @@ use async_graphql::*; +use futures::StreamExt; #[async_std::test] pub async fn test_defer() { - struct Query { - value: i32, + #[derive(Clone)] + struct MyObj; + + #[Object] + impl MyObj { + async fn value(&self) -> i32 { + 20 + } + + async fn obj(&self) -> Deferred { + MyObj.into() + } } + struct Query; + #[Object] impl Query { async fn value(&self) -> Deferred { 10.into() } + + async fn obj(&self) -> Deferred { + MyObj.into() + } } - let schema = Schema::new(Query { value: 10 }, EmptyMutation, EmptySubscription); + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); let query = r#"{ - value + value @defer }"#; assert_eq!( schema.execute(&query).await.unwrap().data, @@ -23,4 +40,55 @@ pub async fn test_defer() { "value": 10, }) ); + + let query = r#"{ + value @defer + obj @defer { + value + obj @defer { + value + } + } + }"#; + assert_eq!( + schema.execute(&query).await.unwrap().data, + serde_json::json!({ + "value": 10, + "obj": { + "value": 20, + "obj": { + "value": 20 + } + } + }) + ); + + let mut stream = schema.execute_stream(&query).await; + assert_eq!( + stream.next().await.unwrap().unwrap().data, + serde_json::json!({ + "value": null, + "obj": null, + }) + ); + + let next_resp = stream.next().await.unwrap().unwrap(); + assert_eq!(next_resp.path, Some(vec![serde_json::json!("value")])); + assert_eq!(next_resp.data, serde_json::json!(10)); + + let next_resp = stream.next().await.unwrap().unwrap(); + assert_eq!(next_resp.path, Some(vec![serde_json::json!("obj")])); + assert_eq!( + next_resp.data, + serde_json::json!({"value": 20, "obj": null}) + ); + + let next_resp = stream.next().await.unwrap().unwrap(); + assert_eq!( + next_resp.path, + Some(vec![serde_json::json!("obj"), serde_json::json!("obj")]) + ); + assert_eq!(next_resp.data, serde_json::json!({"value": 20})); + + assert!(stream.next().await.is_none()); }