Merge branch 'master' into rework-errors

This commit is contained in:
Sunli 2020-10-01 09:39:47 +08:00
commit 6afac75ccf
12 changed files with 241 additions and 182 deletions

View File

@ -14,8 +14,9 @@ categories = ["network-programming", "asynchronous"]
readme = "README.md"
[features]
default = ["apollo_tracing", "uuid", "bson", "chrono", "chrono-tz", "log", "multipart", "tracing", "url", "unblock", "string_number"]
default = ["apollo_tracing", "apollo_persisted_queries", "uuid", "bson", "chrono", "chrono-tz", "log", "multipart", "tracing", "url", "unblock", "string_number"]
apollo_tracing = ["chrono"]
apollo_persisted_queries = ["lru"]
multipart = ["multer", "bytes", "tempfile"]
unblock = ["blocking"]
string_number = ["num-traits"]
@ -50,6 +51,7 @@ log = { version = "0.4.11", optional = true }
tracing = { version = "0.1.19", optional = true }
url = { version = "2.1.1", optional = true }
num-traits = { version = "0.2.12", optional = true }
lru = { version = "0.6.0", optional = true }
bytes = { version = "0.5.4", optional = true }
multer = { version = "1.2.2", optional = true }

View File

@ -52,21 +52,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in
* Error Extensions
* Apollo Federation
* Batch Queries
# Crate features
This crate offers the following features, all of which are activated by default:
- `apollo_tracing`: Enable the [Apollo tracing extension](extensions/struct.ApolloTracing.html).
- `log`: Enable the [logger extension](extensions/struct.Logger.html).
- `tracing`: Enable the [tracing extension](extensions/struct.Tracing.html).
- `multipart`: Support [sending files over HTTP multipart](http/fn.receive_body.html).
- `unblock`: Support [asynchronous reader for Upload](types/struct.Upload.html)
- `bson`: Integrate with the [`bson` crate](https://crates.io/crates/bson).
- `chrono`: Integrate with the [`chrono` crate](https://crates.io/crates/chrono).
- `chrono-tz`: Integrate with the [`chrono-tz` crate](https://crates.io/crates/chrono-tz).
- `url`: Integrate with the [`url` crate](https://crates.io/crates/url).
- `uuid`: Integrate with the [`uuid` crate](https://crates.io/crates/uuid).
* Apollo Persisted Queries
## Examples

View File

@ -92,7 +92,7 @@ pub fn generate(
parse_graphql_attrs::<args::Argument>(&pat.attrs)?
.unwrap_or_default(),
));
pat.attrs.clear();
remove_graphql_attrs(&mut pat.attrs);
}
(arg, Type::Reference(TypeReference { elem, .. })) => {
if let Type::Path(path) = elem.as_ref() {
@ -279,7 +279,7 @@ pub fn generate(
parse_graphql_attrs::<args::Argument>(&pat.attrs)?
.unwrap_or_default(),
));
pat.attrs.clear();
remove_graphql_attrs(&mut pat.attrs);
}
(arg, Type::Reference(TypeReference { elem, .. })) => {
if let Type::Path(path) = elem.as_ref() {

View File

@ -27,3 +27,4 @@ Comparing Features of Other Rust GraphQL Implementations
| Opentracing | 👍 | ⛔️ |
| Apollo Federation | 👍 | ⛔️ |
| Apollo Tracing | 👍 | ⛔️ |
| Apollo Persisted Queries | 👍 | ⛔️ |

View File

@ -237,163 +237,6 @@ impl<T> ExtendError for Result<T> {
}
}
/*
/// An error processing a GraphQL query.
#[derive(Debug, Error, PartialEq)]
pub enum QueryError {
/// The feature is not supported.
#[error("Not supported.")]
NotSupported,
/// The actual input type did not match the expected input type.
#[error("Expected input type \"{expect}\", found {actual}.")]
ExpectedInputType {
/// The expected input type.
expect: String,
/// The actual input type.
actual: Value,
},
/// Parsing of an input value failed.
#[error("Failed to parse input value: {reason}")]
ParseInputValue {
/// The reason for the failure to resolve.
reason: String,
},
/// A field was not found on an object type.
#[error("Cannot query field \"{field_name}\" on type \"{object}\".")]
FieldNotFound {
/// Field name
field_name: String,
/// Object name
object: String,
},
/// `operation_name` in the request was required but not provided.
#[error("Operation name required in request")]
RequiredOperationName,
/// The operation name was unknown.
#[error("Unknown operation named \"{name}\"")]
UnknownOperationNamed {
/// Operation name for query.
name: String,
},
/// The user attempted to query an object without selecting any subfields.
#[error("Type \"{object}\" must have a selection of subfields.")]
MustHaveSubFields {
/// Object name
object: String,
},
/// The schema does not have mutations.
#[error("Schema is not configured for mutations.")]
NotConfiguredMutations,
/// The schema does not have subscriptions.
#[error("Schema is not configured for subscriptions.")]
NotConfiguredSubscriptions,
/// The value does not exist in the enum.
#[error("Invalid value for enum \"{ty}\".")]
InvalidEnumValue {
/// Enum type name
ty: String,
/// Enum value
value: String,
},
/// A required field in an input object was not present.
#[error("Required field \"{field_name}\" for InputObject \"{object}\" does not exist.")]
RequiredField {
/// Field name
field_name: String,
/// Object name
object: &'static str,
},
/// A variable is used but not defined.
#[error("Variable \"${var_name}\" is not defined")]
VarNotDefined {
/// Variable name
var_name: String,
},
/// A directive was required but not provided.
#[error(
"Directive \"{directive}\" argument \"{arg_name}\" of type \"{arg_type}\" is required, but it was not provided."
)]
RequiredDirectiveArgs {
/// Directive name
directive: &'static str,
/// Argument name
arg_name: &'static str,
/// Argument type
arg_type: &'static str,
},
/// An unknown directive name was encountered.
#[error("Unknown directive \"{name}\".")]
UnknownDirective {
/// Directive name
name: String,
},
/// An unknown fragment was encountered.
#[error("Unknown fragment \"{name}\".")]
UnknownFragment {
/// Fragment name
name: String,
},
/// The query was too complex.
// TODO: Expand on this
#[error("Too complex")]
TooComplex,
/// The query was nested too deep.
#[error("Too deep")]
TooDeep,
/// A field handler errored.
#[error("Failed to resolve field: {err}")]
Error {
/// The error description.
err: String,
/// Extensions to the error provided through the [`ErrorExtensions`](trait.ErrorExtensions)
/// or [`ResultExt`](trait.ResultExt) traits.
extended_error: Option<serde_json::Value>,
},
/// Entity not found.
#[error("Entity not found")]
EntityNotFound,
/// "__typename" must be an existing string.
#[error("\"__typename\" must be an existing string")]
TypeNameNotExists,
}
impl QueryError {
/// Convert this error to a regular `Error` type.
pub fn into_error(self, pos: Pos) -> Error {
Error::Query {
pos,
path: None,
err: self,
}
}
}
*/
/// An error parsing the request.
#[derive(Debug, Error)]
#[non_exhaustive]

View File

@ -0,0 +1,176 @@
//! Apollo persisted queries extension.
use crate::extensions::{Error, Extension, ExtensionContext, ExtensionFactory};
use crate::{Request, Result};
use futures::lock::Mutex;
use serde::Deserialize;
use std::sync::Arc;
#[derive(Deserialize)]
struct PersistedQuery {
version: i32,
#[serde(rename = "sha256Hash")]
sha256_hash: String,
}
/// Cache storage for persisted queries.
#[async_trait::async_trait]
pub trait CacheStorage: Send + Sync + Clone + 'static {
/// Load the query by `key`.
async fn get(&self, key: String) -> Option<String>;
/// Save the query by `key`.
async fn set(&self, key: String, query: String);
}
/// Memory-based LRU cache.
#[derive(Clone)]
pub struct LruCacheStorage(Arc<Mutex<lru::LruCache<String, String>>>);
impl LruCacheStorage {
/// Creates a new LRU Cache that holds at most `cap` items.
pub fn new(cap: usize) -> Self {
Self(Arc::new(Mutex::new(lru::LruCache::new(cap))))
}
}
#[async_trait::async_trait]
impl CacheStorage for LruCacheStorage {
async fn get(&self, key: String) -> Option<String> {
let mut cache = self.0.lock().await;
cache.get(&key).cloned()
}
async fn set(&self, key: String, query: String) {
let mut cache = self.0.lock().await;
cache.put(key, query);
}
}
/// Apollo persisted queries extension.
///
/// [Reference](https://www.apollographql.com/docs/react/api/link/persisted-queries/)
#[cfg_attr(feature = "nightly", doc(cfg(feature = "apollo_persisted_queries")))]
pub struct ApolloPersistedQueries<T>(T);
impl<T: CacheStorage> ApolloPersistedQueries<T> {
/// Creates an apollo persisted queries extension.
pub fn new(cache_storage: T) -> ApolloPersistedQueries<T> {
Self(cache_storage)
}
}
impl<T: CacheStorage> ExtensionFactory for ApolloPersistedQueries<T> {
fn create(&self) -> Box<dyn Extension> {
Box::new(ApolloPersistedQueriesExtension {
storage: self.0.clone(),
})
}
}
struct ApolloPersistedQueriesExtension<T> {
storage: T,
}
#[async_trait::async_trait]
impl<T: CacheStorage> Extension for ApolloPersistedQueriesExtension<T> {
async fn prepare_request(
&mut self,
_ctx: &ExtensionContext<'_>,
mut request: Request,
) -> Result<Request> {
if let Some(value) = request.extensions.remove("persistedQuery") {
let persisted_query: PersistedQuery = serde_json::from_value(value).map_err(|_| {
Error::Other("Invalid \"PersistedQuery\" extension configuration.".to_string())
})?;
if persisted_query.version != 1 {
return Err(Error::Other (
format!("Only the \"PersistedQuery\" extension of version \"1\" is supported, and the current version is \"{}\".", persisted_query.version),
));
}
if request.query.is_empty() {
if let Some(query) = self.storage.get(persisted_query.sha256_hash).await {
Ok(Request { query, ..request })
} else {
Err(Error::Other("PersistedQueryNotFound".to_string()))
}
} else {
self.storage
.set(persisted_query.sha256_hash, request.query.clone())
.await;
Ok(request)
}
} else {
Ok(request)
}
}
}
#[cfg(test)]
mod tests {
#[async_std::test]
async fn test() {
use super::*;
use crate::*;
struct Query;
#[Object(internal)]
impl Query {
async fn value(&self) -> i32 {
100
}
}
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
.extension(ApolloPersistedQueries::new(LruCacheStorage::new(256)))
.finish();
let mut request = Request::new("{ value }");
request.extensions.insert(
"persistedQuery".to_string(),
serde_json::json!({
"version": 1,
"sha256Hash": "abc",
}),
);
assert_eq!(
schema.execute(request).await.into_result().unwrap().data,
serde_json::json!({
"value": 100
})
);
let mut request = Request::new("");
request.extensions.insert(
"persistedQuery".to_string(),
serde_json::json!({
"version": 1,
"sha256Hash": "abc",
}),
);
assert_eq!(
schema.execute(request).await.into_result().unwrap().data,
serde_json::json!({
"value": 100
})
);
let mut request = Request::new("");
request.extensions.insert(
"persistedQuery".to_string(),
serde_json::json!({
"version": 1,
"sha256Hash": "def",
}),
);
assert_eq!(
schema.execute(request).await.into_result().unwrap_err(),
Error::Other("PersistedQueryNotFound".to_string())
);
}
}

View File

@ -101,6 +101,12 @@ impl Extension for LoggerExtension {
write!(f, "variables: {}", self.log.variables)?;
write!(f, "{}", self.e.message)
}
Error::Other(err) => error!(
target: "async-graphql", "[OtherError] query: \"{}\", variables: {}, {}",
self.query,
self.variables,
err
),
}
error!(

View File

@ -1,5 +1,7 @@
//! Extensions for schema
#[cfg(feature = "apollo_persisted_queries")]
pub mod apollo_persisted_queries;
#[cfg(feature = "apollo_tracing")]
mod apollo_tracing;
#[cfg(feature = "log")]
@ -8,7 +10,7 @@ mod logger;
mod tracing;
use crate::context::{QueryPathNode, ResolveId};
use crate::{Data, Result, ServerError, ServerResult, Variables};
use crate::{Data, Request, Result, ServerError, ServerResult, Variables};
#[cfg(feature = "apollo_tracing")]
pub use self::apollo_tracing::ApolloTracing;
@ -88,6 +90,7 @@ pub struct ResolveInfo<'a> {
}
/// Represents a GraphQL extension
#[async_trait::async_trait]
#[allow(unused_variables)]
pub trait Extension: Sync + Send + 'static {
/// If this extension needs to output data to query results, you need to specify a name.
@ -95,6 +98,15 @@ pub trait Extension: Sync + Send + 'static {
None
}
/// Called at the prepare request
async fn prepare_request(
&mut self,
ctx: &ExtensionContext<'_>,
request: Request,
) -> ServerResult<Request> {
Ok(request)
}
/// Called at the begin of the parse.
fn parse_start(
&mut self,
@ -158,7 +170,20 @@ impl<T> ErrorLogger for Result<T, Vec<ServerError>> {
}
}
#[async_trait::async_trait]
impl Extension for Extensions {
async fn prepare_request(
&mut self,
ctx: &ExtensionContext<'_>,
request: Request,
) -> ServerResult<Request> {
let mut request = request;
for e in self.0.iter_mut() {
request = e.prepare_request(ctx, request).await?;
}
Ok(request)
}
fn parse_start(
&mut self,
ctx: &ExtensionContext<'_>,

View File

@ -48,12 +48,14 @@
//! * Error Extensions
//! * Apollo Federation
//! * Batch Queries
//! * Apollo Persisted Queries
//!
//! # Crate features
//!
//! This crate offers the following features, all of which are activated by default:
//!
//! - `apollo_tracing`: Enable the [Apollo tracing extension](extensions/struct.ApolloTracing.html).
//! - `apollo_persisted_queries`: Enable the [Apollo persisted queries extension](extensions/apollo_persisted_queries/struct.ApolloPersistedQueries.html).
//! - `log`: Enable the [logger extension](extensions/struct.Logger.html).
//! - `tracing`: Enable the [tracing extension](extensions/struct.Tracing.html).
//! - `multipart`: Support [sending files over HTTP multipart](http/fn.receive_body.html).

View File

@ -2,6 +2,7 @@ use crate::parser::types::UploadValue;
use crate::{Data, ParseRequestError, Value, Variables};
use serde::{Deserialize, Deserializer};
use std::any::Any;
use std::collections::HashMap;
use std::fmt::{self, Debug, Formatter};
use std::fs::File;
@ -29,6 +30,10 @@ pub struct Request {
/// **This data is only valid for this request**
#[serde(skip)]
pub data: Data,
/// The extensions config of the request.
#[serde(default)]
pub extensions: HashMap<String, serde_json::Value>,
}
fn deserialize_variables<'de, D: Deserializer<'de>>(
@ -45,6 +50,7 @@ impl Request {
operation_name: None,
variables: Variables::default(),
data: Data::default(),
extensions: Default::default(),
}
}

View File

@ -9,7 +9,7 @@ use std::pin::Pin;
/// A GraphQL container.
///
/// This helper trait allows the type to call `resolve_object` on itself in its
/// This helper trait allows the type to call `resolve_container` on itself in its
/// `OutputValueType::resolve` implementation.
#[async_trait::async_trait]
pub trait ContainerType: OutputValueType {
@ -114,11 +114,11 @@ pub async fn resolve_container_serial<'a, T: ContainerType + Send + Sync>(
type BoxFieldFuture<'a> =
Pin<Box<dyn Future<Output = ServerResult<(String, serde_json::Value)>> + 'a + Send>>;
/// A set of fields on an object that are being selected.
/// A set of fields on an container that are being selected.
pub struct Fields<'a>(Vec<BoxFieldFuture<'a>>);
impl<'a> Fields<'a> {
/// Add another set of fields to this set of fields using the given object.
/// Add another set of fields to this set of fields using the given container.
pub fn add_set<T: ContainerType + Send + Sync>(
&mut self,
ctx: &ContextSelectionSet<'a>,

View File

@ -319,7 +319,7 @@ where
Self::create_registry().export_sdl(false)
}
fn prepare_request(
async fn prepare_request(
&self,
request: Request,
) -> Result<(QueryEnvInner, CacheControl), Vec<ServerError>> {
@ -331,6 +331,18 @@ where
.map(|factory| factory.create())
.collect_vec(),
));
let request = extensions
.lock()
.prepare_request(
&ExtensionContext {
schema_data: &self.env.data,
query_data: &Default::default(),
},
request,
)
.await?;
let ctx_extension = ExtensionContext {
schema_data: &self.env.data,
query_data: &request.data,
@ -454,7 +466,7 @@ where
/// Execute an GraphQL query.
pub async fn execute(&self, request: impl Into<Request>) -> Response {
let request = request.into();
match self.prepare_request(request) {
match self.prepare_request(request).await {
Ok((env, cache_control)) => self
.execute_once(QueryEnv::new(env))
.await
@ -485,7 +497,7 @@ where
async_stream::stream! {
let request = request.into();
let (mut env, cache_control) = match schema.prepare_request(request) {
let (mut env, cache_control) = match schema.prepare_request(request).await {
Ok(res) => res,
Err(errors) => {
yield Response::from_errors(errors);