Merge branch 'master' of github.com-koxiaet:async-graphql/async-graphql into master
This commit is contained in:
commit
d52268523f
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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的集成
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
46
src/base.rs
46
src/base.rs
|
@ -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()
|
||||
|
|
|
@ -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
141
src/http/batch.rs
Normal 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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
20
src/lib.rs
20
src/lib.rs
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"}]"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(®istry_field.cache_control);
|
||||
*self.cache_control = self.cache_control.merge(®istry_field.cache_control);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
33
tests/batch_request.rs
Normal file
33
tests/batch_request.rs
Normal 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
72
tests/error_ext.rs
Normal 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
|
||||
}
|
||||
}]
|
||||
})
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user