diff --git a/Cargo.toml b/Cargo.toml index 045aed8f..6420a5f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } diff --git a/README.md b/README.md index 43dded53..24223f8b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/derive/src/object.rs b/derive/src/object.rs index 56a51cb7..df950b9a 100644 --- a/derive/src/object.rs +++ b/derive/src/object.rs @@ -92,7 +92,7 @@ pub fn generate( parse_graphql_attrs::(&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::(&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() { diff --git a/feature-comparison.md b/feature-comparison.md index 7d1719dc..b8c5a7d0 100644 --- a/feature-comparison.md +++ b/feature-comparison.md @@ -27,3 +27,4 @@ Comparing Features of Other Rust GraphQL Implementations | Opentracing | 👍 | ⛔️ | | Apollo Federation | 👍 | ⛔️ | | Apollo Tracing | 👍 | ⛔️ | +| Apollo Persisted Queries | 👍 | ⛔️ | \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index a7fb590b..7cabc3d4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -237,163 +237,6 @@ impl ExtendError for Result { } } -/* -/// 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, - }, - - /// 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] diff --git a/src/extensions/apollo_persisted_queries.rs b/src/extensions/apollo_persisted_queries.rs new file mode 100644 index 00000000..9404bee9 --- /dev/null +++ b/src/extensions/apollo_persisted_queries.rs @@ -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; + + /// Save the query by `key`. + async fn set(&self, key: String, query: String); +} + +/// Memory-based LRU cache. +#[derive(Clone)] +pub struct LruCacheStorage(Arc>>); + +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 { + 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); + +impl ApolloPersistedQueries { + /// Creates an apollo persisted queries extension. + pub fn new(cache_storage: T) -> ApolloPersistedQueries { + Self(cache_storage) + } +} + +impl ExtensionFactory for ApolloPersistedQueries { + fn create(&self) -> Box { + Box::new(ApolloPersistedQueriesExtension { + storage: self.0.clone(), + }) + } +} + +struct ApolloPersistedQueriesExtension { + storage: T, +} + +#[async_trait::async_trait] +impl Extension for ApolloPersistedQueriesExtension { + async fn prepare_request( + &mut self, + _ctx: &ExtensionContext<'_>, + mut request: Request, + ) -> Result { + 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()) + ); + } +} diff --git a/src/extensions/logger.rs b/src/extensions/logger.rs index c40a0905..c8b96ca7 100644 --- a/src/extensions/logger.rs +++ b/src/extensions/logger.rs @@ -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!( diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs index 5533a824..786e7456 100644 --- a/src/extensions/mod.rs +++ b/src/extensions/mod.rs @@ -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 { + Ok(request) + } + /// Called at the begin of the parse. fn parse_start( &mut self, @@ -158,7 +170,20 @@ impl ErrorLogger for Result> { } } +#[async_trait::async_trait] impl Extension for Extensions { + async fn prepare_request( + &mut self, + ctx: &ExtensionContext<'_>, + request: Request, + ) -> ServerResult { + 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<'_>, diff --git a/src/lib.rs b/src/lib.rs index e22cab83..0c182382 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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). diff --git a/src/request.rs b/src/request.rs index 57646eae..4a6ca6ed 100644 --- a/src/request.rs +++ b/src/request.rs @@ -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, } 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(), } } diff --git a/src/resolver_utils/container.rs b/src/resolver_utils/container.rs index c6dc3bbf..b035039d 100644 --- a/src/resolver_utils/container.rs +++ b/src/resolver_utils/container.rs @@ -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> + '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>); 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( &mut self, ctx: &ContextSelectionSet<'a>, diff --git a/src/schema.rs b/src/schema.rs index 77e68b75..ed850030 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -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> { @@ -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) -> 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);