Merge branch 'master' of github.com-koxiaet:async-graphql/async-graphql into master

This commit is contained in:
Koxiaet 2020-09-17 19:23:15 +01:00
commit d52268523f
33 changed files with 471 additions and 120 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql"
version = "2.0.0-alpha.8"
version = "2.0.0-alpha.11"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
description = "A GraphQL server library implemented in Rust"
@ -21,8 +21,8 @@ multipart = ["multer", "bytes", "tempfile"]
nightly = []
[dependencies]
async-graphql-derive = { path = "derive", version = "2.0.0-alpha.8" }
async-graphql-parser = { path = "parser", version = "2.0.0-alpha.8" }
async-graphql-derive = { path = "derive", version = "2.0.0-alpha.11" }
async-graphql-parser = { path = "parser", version = "2.0.0-alpha.11" }
async-stream = "0.3"
async-trait = "0.1.30"

View File

@ -51,6 +51,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in
* Limit query complexity/depth
* Error Extensions
* Apollo Federation
* Batch Queries
## Examples

View File

@ -1,6 +1,6 @@
[package]
name = "graphql-benchmark"
version = "2.0.0-alpha.8"
version = "2.0.0-alpha.11"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"

View File

@ -1,6 +1,6 @@
[package]
name = "chat"
version = "2.0.0-alpha.8"
version = "2.0.0-alpha.11"
authors = ["Ivan Plesskih <terma95@gmail.com>"]
edition = "2018"

View File

@ -1,6 +1,6 @@
[package]
name = "simple"
version = "2.0.0-alpha.8"
version = "2.0.0-alpha.11"
authors = ["Ivan Plesskih <terma95@gmail.com>"]
edition = "2018"

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-derive"
version = "2.0.0-alpha.8"
version = "2.0.0-alpha.11"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
description = "Macros for async-graphql"
@ -16,7 +16,7 @@ categories = ["network-programming", "asynchronous"]
proc-macro = true
[dependencies]
async-graphql-parser = { path = "../parser", version = "2.0.0-alpha.8" }
async-graphql-parser = { path = "../parser", version = "2.0.0-alpha.11" }
proc-macro2 = "1.0.6"
syn = { version = "1.0.20", features = ["full", "extra-traits"] }
quote = "1.0.3"

View File

@ -77,7 +77,7 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result
type_into_impls.push(quote! {
#[allow(clippy::all, clippy::pedantic)]
impl #generics From<#p> for #ident #generics {
impl #generics ::std::convert::From<#p> for #ident #generics {
fn from(obj: #p) -> Self {
#ident::#enum_name(obj)
}

View File

@ -70,7 +70,7 @@ pub fn generate(union_args: &args::Interface, input: &DeriveInput) -> Result<Tok
enum_names.push(enum_name);
type_into_impls.push(quote! {
#[allow(clippy::all, clippy::pedantic)]
impl #generics From<#p> for #ident #generics {
impl #generics ::std::convert::From<#p> for #ident #generics {
fn from(obj: #p) -> Self {
#ident::#enum_name(obj)
}

View File

@ -38,15 +38,13 @@ In our example, there is only a Query without a Mutation or Subscription, so we
```rust
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let res = schema.execute("{ add(a: 10, b: 20) }");
let res = schema.execute("{ add(a: 10, b: 20) }").await;
```
## Output the query results as JSON
`Schema::execute` returns `async_graphql::Result` with `async_graphql::http::GQLResponse` wrapped, and it can be directly converted to JSON.
```rust
let json = serde_json::to_string(&async_graphql::http::GQLResponse(res));
let json = serde_json::to_string(&res);
```
## Web server integration

View File

@ -39,15 +39,13 @@ impl Query {
```rust
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let res = schema.execute("{ add(a: 10, b: 20) }");
let res = schema.execute("{ add(a: 10, b: 20) }").await;
```
## 把查询结果输出为JSON
查询返回的`async_graphql::Result`用`async_graphql::http::GQLResponse`包装起来就能直接转换为JSON。
```rust
let json = serde_json::to_string(&async_graphql::http::GQLResponse(res));
let json = serde_json::to_string(&res);
```
## 和Web Server的集成

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-actix-web"
version = "2.0.0-alpha.8"
version = "2.0.0-alpha.11"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
description = "async-graphql for actix-web"
@ -13,7 +13,7 @@ keywords = ["futures", "async", "graphql"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql = { path = "../..", version = "2.0.0-alpha.8" }
async-graphql = { path = "../..", version = "2.0.0-alpha.11" }
actix-web = "3.0.0"
actix-web-actors = "3.0.0"
actix-http = "2.0.0"

View File

@ -8,7 +8,7 @@ mod subscription;
use actix_web::dev::{HttpResponseBuilder, Payload, PayloadStream};
use actix_web::http::StatusCode;
use actix_web::{http, web, Error, FromRequest, HttpRequest, HttpResponse, Responder};
use async_graphql::http::{receive_body, MultipartOptions};
use async_graphql::http::MultipartOptions;
use async_graphql::{ParseRequestError, Request, Response};
use futures::channel::mpsc;
use futures::future::Ready;
@ -67,7 +67,7 @@ impl FromRequest for GQLRequest {
Box::pin(async move {
Ok(GQLRequest(
receive_body(
async_graphql::http::receive_body(
content_type,
rx.map_err(|err| io::Error::new(ErrorKind::Other, err))
.into_async_read(),

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-rocket"
version = "2.0.0-alpha.8"
version = "2.0.0-alpha.11"
authors = ["Daniel Wiesenberg <daniel@simplificAR.io>"]
edition = "2018"
description = "async-graphql for Rocket.rs"
@ -14,7 +14,7 @@ keywords = ["futures", "async", "graphql", "rocket"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql = { path = "../..", version = "2.0.0-alpha.8" }
async-graphql = { path = "../..", version = "2.0.0-alpha.11" }
rocket = { git = "https://github.com/SergioBenitez/Rocket/", rev = "dc2c6ec", default-features = false } #TODO: Change to Cargo crate, when Rocket 0.5.0 is released
log = "0.4.11"
yansi = "0.5.0"

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-tide"
version = "2.0.0-alpha.8"
version = "2.0.0-alpha.11"
authors = ["vkill <vkill.net@gmail.com>"]
edition = "2018"
description = "async-graphql for tide"
@ -13,7 +13,7 @@ keywords = ["futures", "async", "graphql"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql = { path = "../..", version = "2.0.0-alpha.8" }
async-graphql = { path = "../..", version = "2.0.0-alpha.11" }
tide = "0.13.0"
async-trait = "0.1.36"
serde_json = "1.0.56"

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-warp"
version = "2.0.0-alpha.8"
version = "2.0.0-alpha.11"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
description = "async-graphql for warp"
@ -13,7 +13,7 @@ keywords = ["futures", "async", "graphql"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql = { path = "../..", version = "2.0.0-alpha.8" }
async-graphql = { path = "../..", version = "2.0.0-alpha.11" }
warp = "0.2.2"
futures = "0.3.0"
bytes = "0.5.4"

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-parser"
version = "2.0.0-alpha.8"
version = "2.0.0-alpha.11"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
description = "GraphQL query parser for async-graphql"

View File

@ -4,7 +4,6 @@ use crate::{
registry, ContextSelectionSet, FieldResult, InputValueResult, Positioned, Result, Value,
};
use std::borrow::Cow;
use std::sync::Arc;
/// Represents a GraphQL type
///
@ -113,51 +112,6 @@ impl<T: OutputValueType + Send + Sync> OutputValueType for &T {
}
}
impl<T: Type + Send + Sync> Type for Box<T> {
fn type_name() -> Cow<'static, str> {
T::type_name()
}
fn create_type_info(registry: &mut Registry) -> String {
T::create_type_info(registry)
}
}
#[async_trait::async_trait]
impl<T: OutputValueType + Send + Sync> OutputValueType for Box<T> {
#[allow(clippy::trivially_copy_pass_by_ref)]
#[allow(clippy::borrowed_box)]
async fn resolve(
&self,
ctx: &ContextSelectionSet<'_>,
field: &Positioned<Field>,
) -> Result<serde_json::Value> {
T::resolve(&*self, ctx, field).await
}
}
impl<T: Type + Send + Sync> Type for Arc<T> {
fn type_name() -> Cow<'static, str> {
T::type_name()
}
fn create_type_info(registry: &mut Registry) -> String {
T::create_type_info(registry)
}
}
#[async_trait::async_trait]
impl<T: OutputValueType + Send + Sync> OutputValueType for Arc<T> {
#[allow(clippy::trivially_copy_pass_by_ref)]
async fn resolve(
&self,
ctx: &ContextSelectionSet<'_>,
field: &Positioned<Field>,
) -> Result<serde_json::Value> {
T::resolve(&*self, ctx, field).await
}
}
impl<T: Type> Type for FieldResult<T> {
fn type_name() -> Cow<'static, str> {
T::type_name()

View File

@ -72,7 +72,7 @@ impl FieldError {
}
}
/// An alias for `Result<T, InputValueError>`.
/// An alias for `Result<T, FieldError>`.
pub type FieldResult<T> = std::result::Result<T, FieldError>;
impl<E: Display> From<E> for FieldError {

141
src/http/batch.rs Normal file
View File

@ -0,0 +1,141 @@
#[cfg(feature = "multipart")]
use crate::http::{multipart::ReaderStream, MultipartOptions};
use crate::{BatchRequest, ParseRequestError};
use futures::{AsyncRead, AsyncReadExt};
#[cfg(feature = "multipart")]
use multer::{Constraints, Multipart, SizeLimit};
#[cfg(feature = "multipart")]
use std::{
collections::HashMap,
io::{Seek, SeekFrom, Write},
};
#[cfg(feature = "multipart")]
#[cfg_attr(feature = "nightly", doc(cfg(feature = "multipart")))]
/// Receive a GraphQL batch request from a content type and body.
pub async fn receive_batch_body(
content_type: Option<impl AsRef<str>>,
body: impl AsyncRead + Send + 'static,
opts: MultipartOptions,
) -> Result<BatchRequest, ParseRequestError> {
if let Some(Ok(boundary)) = content_type.map(multer::parse_boundary) {
receive_batch_multipart(body, boundary, opts).await
} else {
receive_batch_json(body).await
}
}
/// Receive a GraphQL batch request from a body as JSON.
pub async fn receive_batch_json(
body: impl AsyncRead + Send + 'static,
) -> Result<BatchRequest, ParseRequestError> {
let mut data = Vec::new();
futures::pin_mut!(body);
body.read_to_end(&mut data)
.await
.map_err(ParseRequestError::Io)?;
Ok(serde_json::from_slice::<BatchRequest>(&data).map_err(ParseRequestError::InvalidRequest)?)
}
#[cfg(feature = "multipart")]
async fn receive_batch_multipart(
body: impl AsyncRead + Send + 'static,
boundary: impl Into<String>,
opts: MultipartOptions,
) -> Result<BatchRequest, ParseRequestError> {
let mut multipart = Multipart::new_with_constraints(
ReaderStream::new(body),
boundary,
Constraints::new().size_limit({
let mut limit = SizeLimit::new();
if let (Some(max_file_size), Some(max_num_files)) =
(opts.max_file_size, opts.max_file_size)
{
limit = limit.whole_stream((max_file_size * max_num_files) as u64);
}
if let Some(max_file_size) = opts.max_file_size {
limit = limit.per_field(max_file_size as u64);
}
limit
}),
);
let mut batch_request = None;
let mut map = None;
let mut files = Vec::new();
while let Some(mut field) = multipart.next_field().await? {
match field.name() {
Some("operations") => {
let request_str = field.text().await?;
batch_request = Some(
serde_json::from_str::<BatchRequest>(&request_str)
.map_err(ParseRequestError::InvalidRequest)?,
);
}
Some("map") => {
let map_str = field.text().await?;
map = Some(
serde_json::from_str::<HashMap<String, Vec<String>>>(&map_str)
.map_err(ParseRequestError::InvalidFilesMap)?,
);
}
_ => {
if let Some(name) = field.name().map(ToString::to_string) {
if let Some(filename) = field.file_name().map(ToString::to_string) {
let content_type = field.content_type().map(|mime| mime.to_string());
let mut file = tempfile::tempfile().map_err(ParseRequestError::Io)?;
while let Some(chunk) = field.chunk().await.unwrap() {
file.write(&chunk).map_err(ParseRequestError::Io)?;
}
file.seek(SeekFrom::Start(0))?;
files.push((name, filename, content_type, file));
}
}
}
}
}
let mut batch_request: BatchRequest =
batch_request.ok_or(ParseRequestError::MissingOperatorsPart)?;
let map = map.as_mut().ok_or(ParseRequestError::MissingMapPart)?;
for (name, filename, content_type, file) in files {
if let Some(var_paths) = map.remove(&name) {
for var_path in var_paths {
match &mut batch_request {
BatchRequest::Single(request) => {
request.set_upload(
&var_path,
filename.clone(),
content_type.clone(),
file.try_clone().unwrap(),
);
}
BatchRequest::Batch(requests) => {
let mut s = var_path.splitn(2, '.');
let idx = s.next().and_then(|idx| idx.parse::<usize>().ok());
let path = s.next();
if let (Some(idx), Some(path)) = (idx, path) {
if let Some(request) = requests.get_mut(idx) {
request.set_upload(
path,
filename.clone(),
content_type.clone(),
file.try_clone().unwrap(),
);
}
}
}
}
}
}
}
if !map.is_empty() {
return Err(ParseRequestError::MissingFiles);
}
Ok(batch_request)
}

View File

@ -1,14 +1,18 @@
//! A helper module that supports HTTP
mod batch;
mod graphiql_source;
#[cfg(feature = "multipart")]
mod multipart;
mod playground_source;
mod websocket;
#[cfg(feature = "multipart")]
pub use batch::receive_batch_body;
pub use batch::receive_batch_json;
pub use graphiql_source::graphiql_source;
#[cfg(feature = "multipart")]
pub use multipart::{receive_multipart, MultipartOptions};
pub use multipart::MultipartOptions;
pub use playground_source::{playground_source, GraphQLPlaygroundConfig};
pub use websocket::WebSocket;
@ -16,19 +20,16 @@ use crate::{ParseRequestError, Request};
use futures::io::AsyncRead;
use futures::AsyncReadExt;
/// Receive a GraphQL request from a content type and body.
///
/// If the content type is multipart it will use `receive_multipart`, otherwise it will use
/// `receive_json`.
#[cfg(feature = "multipart")]
#[cfg_attr(feature = "nightly", doc(cfg(feature = "multipart")))]
/// Receive a GraphQL request from a content type and body.
pub async fn receive_body(
content_type: Option<impl AsRef<str>>,
body: impl AsyncRead + Send + 'static,
opts: MultipartOptions,
) -> Result<Request, ParseRequestError> {
if let Some(Ok(boundary)) = content_type.map(multer::parse_boundary) {
receive_multipart(body, boundary, opts).await
multipart::receive_multipart(body, boundary, opts).await
} else {
receive_json(body).await
}

View File

@ -39,8 +39,7 @@ impl MultipartOptions {
}
/// Receive a multipart request.
#[cfg_attr(feature = "nightly", doc(cfg(feature = "multipart")))]
pub async fn receive_multipart(
pub(crate) async fn receive_multipart(
body: impl AsyncRead + Send + 'static,
boundary: impl Into<String>,
opts: MultipartOptions,
@ -122,7 +121,7 @@ pub async fn receive_multipart(
}
pin_project! {
struct ReaderStream<T> {
pub(crate) struct ReaderStream<T> {
buf: [u8; 2048],
#[pin]
reader: T,
@ -130,7 +129,7 @@ pin_project! {
}
impl<T> ReaderStream<T> {
fn new(reader: T) -> Self {
pub(crate) fn new(reader: T) -> Self {
Self {
buf: [0; 2048],
reader,

View File

@ -47,6 +47,7 @@
//! * Limit query complexity/depth
//! * Error Extensions
//! * Apollo Federation
//! * Batch Queries
//!
//! ## Integrations
//!
@ -157,8 +158,8 @@ pub use error::{
pub use look_ahead::Lookahead;
pub use parser::{types::ConstValue as Value, Pos, Positioned};
pub use registry::CacheControl;
pub use request::Request;
pub use response::Response;
pub use request::{BatchRequest, Request};
pub use response::{BatchResponse, Response};
pub use schema::{Schema, SchemaBuilder, SchemaEnv};
pub use serde_json::Number;
pub use types::*;
@ -292,8 +293,6 @@ pub use async_graphql_derive::GQLObject;
/// Define a GraphQL object with fields
///
/// You can also [derive this](derive.GQLSimpleObject.html).
///
/// *[See also the Book](https://async-graphql.github.io/async-graphql/en/define_simple_object.html).*
///
/// Similar to `Object`, but defined on a structure that automatically generates getters for all fields. For a list of valid field types, see [`Object`](attr.Object.html). All fields are converted to camelCase.
@ -343,8 +342,6 @@ pub use async_graphql_derive::GQLSimpleObject;
/// Define a GraphQL enum
///
/// You can also [derive this](derive.GQLEnum.html).
///
/// *[See also the Book](https://async-graphql.github.io/async-graphql/en/define_enum.html).*
///
/// # Macro parameters
@ -401,9 +398,6 @@ pub use async_graphql_derive::GQLEnum;
/// Define a GraphQL input object
///
///
/// You can also [derive this](derive.GQLInputObject.html).
///
/// *[See also the Book](https://async-graphql.github.io/async-graphql/en/define_input_object.html).*
///
/// # Macro parameters
@ -461,8 +455,6 @@ pub use async_graphql_derive::GQLInputObject;
/// Define a GraphQL interface
///
/// You can also [derive this](derive.GQLInterface.html).
///
/// *[See also the Book](https://async-graphql.github.io/async-graphql/en/define_interface.html).*
///
/// # Macro parameters
@ -591,8 +583,6 @@ pub use async_graphql_derive::GQLInterface;
/// Define a GraphQL union
///
/// You can also [derive this](derive.GQLUnion.html).
///
/// *[See also the Book](https://async-graphql.github.io/async-graphql/en/define_union.html).*
///
/// # Macro parameters
@ -725,8 +715,6 @@ pub use async_graphql_derive::GQLScalar;
/// Define a merged object with multiple object types.
///
/// You can also [derive this](derive.GQLMergedObject.html).
///
/// *[See also the Book](https://async-graphql.github.io/async-graphql/en/merging_objects.html).*
///
/// # Macro parameters
@ -767,8 +755,6 @@ pub use async_graphql_derive::GQLMergedObject;
/// Define a merged subscription with multiple subscription types.
///
/// You can also [derive this](derive.GQLMergedSubscription.html).
///
/// *[See also the Book](https://async-graphql.github.io/async-graphql/en/merging_objects.html).*
///
/// # Macro parameters

View File

@ -63,14 +63,17 @@ impl CacheControl {
}
impl CacheControl {
pub(crate) fn merge(&mut self, other: &CacheControl) {
self.public = self.public && other.public;
self.max_age = if self.max_age == 0 {
other.max_age
} else if other.max_age == 0 {
self.max_age
} else {
self.max_age.min(other.max_age)
};
#[must_use]
pub(crate) fn merge(self, other: &CacheControl) -> CacheControl {
CacheControl {
public: self.public && other.public,
max_age: if self.max_age == 0 {
other.max_age
} else if other.max_age == 0 {
self.max_age
} else {
self.max_age.min(other.max_age)
},
}
}
}

View File

@ -1,6 +1,6 @@
use crate::parser::types::UploadValue;
use crate::{Data, Value, Variables};
use serde::Deserialize;
use serde::{Deserialize, Deserializer};
use std::any::Any;
use std::fs::File;
@ -87,6 +87,47 @@ impl<T: Into<String>> From<T> for Request {
}
}
/// Batch support for GraphQL requests, which is either a single query, or an array of queries
///
/// **Reference:** <https://www.apollographql.com/blog/batching-client-graphql-queries-a685f5bcd41b/>
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum BatchRequest {
/// Single query
Single(Request),
/// Non-empty array of queries
#[serde(deserialize_with = "deserialize_non_empty_vec")]
Batch(Vec<Request>),
}
fn deserialize_non_empty_vec<'de, D, T>(deserializer: D) -> std::result::Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
use serde::de::Error as _;
let v = Vec::<T>::deserialize(deserializer)?;
if v.is_empty() {
Err(D::Error::invalid_length(0, &"a positive integer"))
} else {
Ok(v)
}
}
impl From<Request> for BatchRequest {
fn from(r: Request) -> Self {
BatchRequest::Single(r)
}
}
impl From<Vec<Request>> for BatchRequest {
fn from(r: Vec<Request>) -> Self {
BatchRequest::Batch(r)
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -137,4 +178,45 @@ mod tests {
assert!(request.operation_name.is_none());
assert_eq!(request.query, "{ a b c }");
}
#[test]
fn test_batch_request_single() {
let request: BatchRequest = serde_json::from_value(json! ({
"query": "{ a b c }"
}))
.unwrap();
if let BatchRequest::Single(request) = request {
assert!(request.variables.0.is_empty());
assert!(request.operation_name.is_none());
assert_eq!(request.query, "{ a b c }");
} else {
unreachable!()
}
}
#[test]
fn test_batch_request_batch() {
let request: BatchRequest = serde_json::from_value(json!([
{
"query": "{ a b c }"
},
{
"query": "{ d e }"
}
]))
.unwrap();
if let BatchRequest::Batch(requests) = request {
assert!(requests[0].variables.0.is_empty());
assert!(requests[0].operation_name.is_none());
assert_eq!(requests[0].query, "{ a b c }");
assert!(requests[1].variables.0.is_empty());
assert!(requests[1].operation_name.is_none());
assert_eq!(requests[1].query, "{ d e }");
} else {
unreachable!()
}
}
}

View File

@ -1,4 +1,5 @@
use crate::{CacheControl, Error, Result};
use serde::Serialize;
/// Query response
#[derive(Debug, Default)]
@ -88,3 +89,57 @@ impl From<Error> for Response {
Self::from_error(err)
}
}
/// Response for batchable queries
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum BatchResponse {
/// Response for single queries
Single(Response),
/// Response for batch queries
Batch(Vec<Response>),
}
impl BatchResponse {
/// Get cache control value
pub fn cache_control(&self) -> CacheControl {
match self {
BatchResponse::Single(resp) => resp.cache_control,
BatchResponse::Batch(resp) => resp.iter().fold(CacheControl::default(), |acc, item| {
acc.merge(&item.cache_control)
}),
}
}
/// Returns `true` if all responses are ok.
pub fn is_ok(&self) -> bool {
match self {
BatchResponse::Single(resp) => resp.is_ok(),
BatchResponse::Batch(resp) => resp.iter().all(Response::is_ok),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_batch_response_single() {
let resp = BatchResponse::Single(Response::new(serde_json::Value::Bool(true)));
assert_eq!(serde_json::to_string(&resp).unwrap(), r#"{"data":true}"#);
}
#[test]
fn test_batch_response_batch() {
let resp = BatchResponse::Batch(vec![
Response::new(serde_json::Value::Bool(true)),
Response::new(serde_json::Value::String("1".to_string())),
]);
assert_eq!(
serde_json::to_string(&resp).unwrap(),
r#"[{"data":true},{"data":"1"}]"#
);
}
}

View File

@ -9,8 +9,8 @@ use crate::subscription::collect_subscription_streams;
use crate::types::QueryRoot;
use crate::validation::{check_rules, CheckResult, ValidationMode};
use crate::{
CacheControl, ContextBase, Error, Pos, QueryEnv, QueryError, Request, Response, Result,
SubscriptionType, Type, Variables, ID,
BatchRequest, BatchResponse, CacheControl, ContextBase, Error, Pos, QueryEnv, QueryError,
Request, Response, Result, SubscriptionType, Type, Variables, ID,
};
use async_graphql_parser::types::ExecutableDocumentData;
use futures::stream::{self, Stream, StreamExt};
@ -434,6 +434,19 @@ where
}
}
/// Execute an GraphQL batch query.
pub async fn execute_batch(&self, batch_request: BatchRequest) -> BatchResponse {
match batch_request {
BatchRequest::Single(request) => BatchResponse::Single(self.execute(request).await),
BatchRequest::Batch(requests) => BatchResponse::Batch(
futures::stream::iter(requests.into_iter())
.then(|request| self.execute(request))
.collect()
.await,
),
}
}
pub(crate) fn execute_stream_with_ctx_data(
&self,
request: impl Into<Request> + Send,

View File

@ -1,7 +1,7 @@
use crate::{GQLScalar, InputValueResult, ScalarType, Value};
use serde::de::DeserializeOwned;
/// Any scalar
/// Any scalar (For [Apollo Federation](https://www.apollographql.com/docs/apollo-server/federation/introduction))
///
/// The `Any` scalar is used to pass representations of entities from external services into the root `_entities` field for execution.
#[derive(Clone, PartialEq, Debug)]

View File

@ -72,6 +72,16 @@ impl<T> MaybeUndefined<T> {
}
}
/// Returns true if the MaybeUndefined<T> is value.
#[inline]
pub fn is_value(&self) -> bool {
if let MaybeUndefined::Value(_) = self {
true
} else {
false
}
}
/// Borrow the value, returns `None` if the value is `undefined` or `null`, otherwise returns `Some(T)`.
#[inline]
pub fn value(&self) -> Option<&T> {

View File

@ -41,7 +41,7 @@ impl<A: Type, B: Type> Type for MergedObject<A, B> {
}) = registry.types.remove(&*A::type_name())
{
fields.extend(a_fields);
cc.merge(&a_cc);
cc = cc.merge(&a_cc);
}
B::create_type_info(registry);
@ -52,7 +52,7 @@ impl<A: Type, B: Type> Type for MergedObject<A, B> {
}) = registry.types.remove(&*B::type_name())
{
fields.extend(b_fields);
cc.merge(&b_cc);
cc = cc.merge(&b_cc);
}
MetaType::Object {

View File

@ -55,6 +55,11 @@ impl Upload {
self.0.content_type.as_deref()
}
/// Returns the size of the file, in bytes.
pub fn size(&self) -> std::io::Result<u64> {
self.0.content.metadata().map(|meta| meta.len())
}
/// Convert to a `Read`.
///
/// **Note**: this is a *synchronous/blocking* reader.

View File

@ -15,7 +15,7 @@ impl<'ctx, 'a> Visitor<'ctx> for CacheControlCalculate<'a> {
) {
if let Some(current_type) = ctx.current_type() {
if let MetaType::Object { cache_control, .. } = current_type {
self.cache_control.merge(cache_control);
*self.cache_control = self.cache_control.merge(cache_control);
}
}
}
@ -25,7 +25,7 @@ impl<'ctx, 'a> Visitor<'ctx> for CacheControlCalculate<'a> {
.parent_type()
.and_then(|parent| parent.field_by_name(&field.node.name.node))
{
self.cache_control.merge(&registry_field.cache_control);
*self.cache_control = self.cache_control.merge(&registry_field.cache_control);
}
}
}

33
tests/batch_request.rs Normal file
View File

@ -0,0 +1,33 @@
use async_graphql::*;
#[async_std::test]
pub async fn test_batch_request() {
struct Query;
#[GQLObject]
impl Query {
async fn value(&self, a: i32, b: i32) -> i32 {
a + b
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let batch: BatchRequest = vec![
Request::new("{ value(a: 10, b: 20) }"),
Request::new("{ value(a: 30, b: 40) }"),
Request::new("{ value1 }"),
]
.into();
let resp = schema.execute_batch(batch).await;
assert_eq!(
serde_json::to_value(&resp).unwrap(),
serde_json::json!([
{"data": { "value": 30 }},
{"data": { "value": 70 }},
{"errors": [{
"message": r#"Unknown field "value1" on type "Query". Did you mean "value"?"#,
"locations": [{"line": 1, "column": 3}]
}]},
])
);
}

72
tests/error_ext.rs Normal file
View File

@ -0,0 +1,72 @@
use async_graphql::*;
#[async_std::test]
pub async fn test_error_extensions() {
struct Query;
#[GQLObject]
impl Query {
async fn extend_err(&self) -> FieldResult<i32> {
Err("my error".extend_with(|err| {
serde_json::json!({
"msg": err,
"code": 100
})
}))
}
async fn extend_result(&self) -> FieldResult<i32> {
Err(FieldError::from("my error"))
.extend_err(|_| {
serde_json::json!({
"msg": "my error",
"code": 100
})
})
.extend_err(|_| {
serde_json::json!({
"code2": 20
})
})
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
assert_eq!(
serde_json::to_value(&schema.execute("{ extendErr }").await).unwrap(),
serde_json::json!({
"errors": [{
"message": "my error",
"locations": [{
"column": 3,
"line": 1,
}],
"path": ["extendErr"],
"extensions": {
"msg": "my error",
"code": 100
}
}]
})
);
assert_eq!(
serde_json::to_value(&schema.execute("{ extendResult }").await).unwrap(),
serde_json::json!({
"errors": [{
"message": "my error",
"locations": [{
"column": 3,
"line": 1,
}],
"path": ["extendResult"],
"extensions": {
"msg": "my error",
"code": 100,
"code2": 20
}
}]
})
);
}