diff --git a/book/src/serve/index.md b/book/src/serve/index.md index 75dd7f8c2..87eb58153 100644 --- a/book/src/serve/index.md +++ b/book/src/serve/index.md @@ -54,7 +54,7 @@ In the [Juniper] ecosystem, both implementations are provided by the [`juniper_g [`subscriptions-transport-ws` npm package]: https://npmjs.com/package/subscriptions-transport-ws [`warp`]: https://docs.rs/warp [Apollo]: https://www.apollographql.com -[GraphiQL]: https://github.com/graphql/graphiql +ยง [GraphQL]: https://graphql.org [GraphQL Playground]: https://github.com/prisma/graphql-playground [HTTP]: https://en.wikipedia.org/wiki/HTTP diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index b7a19c4e3..31f127451 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -18,8 +18,13 @@ All user visible changes to `juniper` crate will be documented in this file. Thi - Added `description` field to `ast::Operation`, `ast::Fragment` and `ast::VariableDefinition`. ([#1349], [graphql/graphql-spec#1170]) - Renamed `ast::VariableDefinitions` to `ast::VariablesDefinition`: ([#1353], [graphql/graphql-spec#916]) - Renamed `ast::Operation::variable_definitions` field to `variables_definition`. + - Added `extenstions` field to `http::GraphQLRequest`. ([#1356], [graphql/graphql-spec#976]) + - Added `Ext` type parameter to `http::GraphQLRequest` and `http::GraphQLBatchRequest` defaulting to `Variables`. ([#1356], [graphql/graphql-spec#976]) - Changed `ScalarToken::String` to contain raw quoted and escaped `StringLiteral` (was unquoted but escaped string before). ([#1349]) - Added `LexerError::UnterminatedBlockString` variant. ([#1349]) +- `http::GraphQLRequest`: + - Removed `new()` constructor in favor of constructing it directly. ([#1356]) + - Removed deprecated `operation_name()` method in favor of direct `operation_name` field. ([#1356]) ### Added @@ -72,12 +77,14 @@ All user visible changes to `juniper` crate will be documented in this file. Thi [#1353]: /../../pull/1353 [#1354]: /../../pull/1354 [#1355]: /../../pull/1355 +[#1356]: /../../pull/1356 [graphql/graphql-spec#525]: https://github.com/graphql/graphql-spec/pull/525 [graphql/graphql-spec#687]: https://github.com/graphql/graphql-spec/issues/687 [graphql/graphql-spec#805]: https://github.com/graphql/graphql-spec/pull/805 [graphql/graphql-spec#825]: https://github.com/graphql/graphql-spec/pull/825 [graphql/graphql-spec#849]: https://github.com/graphql/graphql-spec/pull/849 [graphql/graphql-spec#916]: https://github.com/graphql/graphql-spec/pull/916 +[graphql/graphql-spec#976]: https://github.com/graphql/graphql-spec/pull/976 [graphql/graphql-spec#1040]: https://github.com/graphql/graphql-spec/pull/1040 [graphql/graphql-spec#1142]: https://github.com/graphql/graphql-spec/pull/1142 [graphql/graphql-spec#1170]: https://github.com/graphql/graphql-spec/pull/1170 diff --git a/juniper/src/executor/mod.rs b/juniper/src/executor/mod.rs index 33a2477d0..8554b6707 100644 --- a/juniper/src/executor/mod.rs +++ b/juniper/src/executor/mod.rs @@ -379,6 +379,98 @@ where } } +/// Execution context for resolving GraphQL types. +pub trait Context {} + +impl Context for &C {} + +/// Injects the provided value into this [`Context`]. +/// +/// # Implementation +/// +/// Default implementation does nothing. +/// +/// This trait should be implemented for a [`Context`] implementation to be populated with +/// [request `extensions`][0]. +/// +/// ```rust +/// # use juniper::{ +/// # Context, DefaultScalarValue, EmptyMutation, EmptySubscription, Inject, RootNode, +/// # graphql_object, graphql_value, http::GraphQLRequest, +/// # }; +/// # use serde::{Deserialize, Serialize}; +/// # use serde_json::json; +/// # +/// #[derive(Deserialize, Serialize)] +/// #[serde(rename_all = "camelCase")] +/// struct CustomExtensions { +/// persisted_query: PersistedQueryExtensions, +/// } +/// #[derive(Deserialize, Serialize)] +/// #[serde(rename_all = "camelCase")] +/// struct PersistedQueryExtensions { +/// sha256_hash: Box, +/// } +/// +/// type CustomGraphQLRequest = GraphQLRequest; +/// +/// #[derive(Default)] +/// struct CustomContext { +/// persisted_query_sha256_hash: Option>, +/// } +/// impl Context for CustomContext {} +/// impl Inject for CustomContext { +/// fn inject(&mut self, extensions: &CustomExtensions) { +/// self.persisted_query_sha256_hash = Some(extensions.persisted_query.sha256_hash.clone()); +/// } +/// } +/// +/// struct Query; +/// +/// #[graphql_object] +/// impl Query { +/// fn is_persisted_query(context: &CustomContext) -> bool { +/// context.persisted_query_sha256_hash.is_some() +/// } +/// } +/// # +/// # type Schema = RootNode< +/// # Query, EmptyMutation, EmptySubscription, +/// # >; +/// # +/// # #[tokio::main] +/// # async fn main() { +/// # let request: CustomGraphQLRequest = serde_json::from_value(json!({ +/// # "query": "{ isPersistedQuery }", +/// # "extensions": { +/// # "persistedQuery": { +/// # "sha256Hash": +/// # "c205cf782b5c43c3fc67b5233445b78fbea47b99a0302cf31bda2a8e2162e1e6", +/// # }, +/// # }, +/// # })).unwrap(); +/// # let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()); +/// # let context = CustomContext::default(); +/// # +/// # assert_eq!( +/// # request.execute(&schema, context).await.into_result(), +/// # Ok((graphql_value!({"isPersistedQuery": true}), vec![])), +/// # ); +/// # } +/// ``` +/// +/// [0]: https://spec.graphql.org/September2025#sel-FANHLBBgBBvC0vW +pub trait Inject { + /// Injects the provided `value` into this [`Context`]. + /// + /// Default implementation does nothing. + fn inject(&mut self, value: &V) { + _ = value; + } +} + +impl Inject for () {} + /// Conversion trait for context types /// /// Used to support different context types for different parts of an @@ -394,12 +486,6 @@ pub trait FromContext { /// Perform the conversion fn from(value: &T) -> &Self; } - -/// Marker trait for types that can act as context objects for `GraphQL` types. -pub trait Context {} - -impl Context for &C {} - static NULL_CONTEXT: () = (); impl FromContext for () { diff --git a/juniper/src/http/mod.rs b/juniper/src/http/mod.rs index 71f97d581..380725f35 100644 --- a/juniper/src/http/mod.rs +++ b/juniper/src/http/mod.rs @@ -12,7 +12,7 @@ use crate::{ FieldError, GraphQLError, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, RootNode, Value, Variables, ast::InputValue, - executor::{ExecutionError, ValuesStream}, + executor::{ExecutionError, Inject, ValuesStream}, value::{DefaultScalarValue, ScalarValue}, }; @@ -24,7 +24,9 @@ use crate::{ /// For GET, you will need to parse the query string and extract "query", /// "operationName", and "variables" manually. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct GraphQLRequest +// Should be specified as top-level, otherwise `serde` infers incorrect `Ext: Default` bound. +#[serde(bound(deserialize = "Ext: Deserialize<'de>"))] +pub struct GraphQLRequest> where S: ScalarValue, { @@ -42,19 +44,18 @@ where serialize = "InputValue: Serialize", ))] pub variables: Option>, + + /// Optional implementation-specific additional information (as per [spec]). + /// + /// [spec]: https://spec.graphql.org/September2025#sel-FANHLBBgBBvC0vW + #[serde(default, skip_serializing_if = "Option::is_none")] + pub extensions: Option, } -impl GraphQLRequest +impl GraphQLRequest where S: ScalarValue, { - // TODO: Remove in 0.17 `juniper` version. - /// Returns the `operation_name` associated with this request. - #[deprecated(since = "0.16.0", note = "Use the direct field access instead.")] - pub fn operation_name(&self) -> Option<&str> { - self.operation_name.as_deref() - } - /// Returns operation [`Variables`] defined withing this request. pub fn variables(&self) -> Variables { self.variables @@ -66,19 +67,6 @@ where .unwrap_or_default() } - /// Construct a new GraphQL request from parts - pub fn new( - query: String, - operation_name: Option, - variables: Option>, - ) -> Self { - Self { - query, - operation_name, - variables, - } - } - /// Execute a GraphQL request synchronously using the specified schema and context /// /// This is a simple wrapper around the `execute_sync` function exposed at the @@ -86,21 +74,20 @@ where pub fn execute_sync( &self, root_node: &RootNode, - context: &QueryT::Context, + mut context: QueryT::Context, ) -> GraphQLResponse where - S: ScalarValue, - QueryT: GraphQLType, + QueryT: GraphQLType>, MutationT: GraphQLType, SubscriptionT: GraphQLType, { - GraphQLResponse(crate::execute_sync( - &self.query, - self.operation_name.as_deref(), - root_node, - &self.variables(), - context, - )) + let op = self.operation_name.as_deref(); + let vars = &self.variables(); + if let Some(extensions) = self.extensions.as_ref() { + context.inject(extensions); + } + let res = crate::execute_sync(&self.query, op, root_node, vars, &context); + GraphQLResponse(res) } /// Execute a GraphQL request using the specified schema and context @@ -110,25 +97,127 @@ where pub async fn execute<'a, QueryT, MutationT, SubscriptionT>( &'a self, root_node: &'a RootNode, - context: &'a QueryT::Context, + mut context: QueryT::Context, ) -> GraphQLResponse where - QueryT: GraphQLTypeAsync, - QueryT::TypeInfo: Sync, - QueryT::Context: Sync, - MutationT: GraphQLTypeAsync, - MutationT::TypeInfo: Sync, - SubscriptionT: GraphQLType + Sync, - SubscriptionT::TypeInfo: Sync, - S: ScalarValue + Send + Sync, + S: Send + Sync, + QueryT: GraphQLTypeAsync + Sync>, + MutationT: GraphQLTypeAsync, + SubscriptionT: GraphQLType + Sync, { let op = self.operation_name.as_deref(); let vars = &self.variables(); - let res = crate::execute(&self.query, op, root_node, vars, context).await; + if let Some(extensions) = self.extensions.as_ref() { + context.inject(extensions); + } + let res = crate::execute(&self.query, op, root_node, vars, &context).await; GraphQLResponse(res) } } +/// Simple wrapper around GraphQLRequest to allow the handling of Batch requests. +#[derive(Debug, Deserialize, PartialEq)] +#[serde(untagged)] +#[serde(bound = "GraphQLRequest: Deserialize<'de>")] +pub enum GraphQLBatchRequest> +where + S: ScalarValue, +{ + /// A single operation request. + Single(GraphQLRequest), + + /// A batch operation request. + /// + /// Empty batch is considered as invalid value, so cannot be deserialized. + #[serde(deserialize_with = "deserialize_non_empty_batch")] + Batch(Vec>), +} + +fn deserialize_non_empty_batch<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: de::Deserializer<'de>, + T: Deserialize<'de>, +{ + use de::Error as _; + + let v = Vec::::deserialize(deserializer)?; + if v.is_empty() { + Err(D::Error::invalid_length( + 0, + &"non-empty batch of GraphQL requests", + )) + } else { + Ok(v) + } +} + +impl GraphQLBatchRequest +where + S: ScalarValue, +{ + /// Execute a GraphQL batch request synchronously using the specified schema and context + /// + /// This is a simple wrapper around the `execute_sync` function exposed in GraphQLRequest. + pub fn execute_sync<'a, QueryT, MutationT, SubscriptionT>( + &'a self, + root_node: &'a RootNode, + context: QueryT::Context, + ) -> GraphQLBatchResponse + where + QueryT: GraphQLType + Clone>, + MutationT: GraphQLType, + SubscriptionT: GraphQLType, + { + match self { + Self::Single(req) => GraphQLBatchResponse::Single(req.execute_sync(root_node, context)), + Self::Batch(reqs) => GraphQLBatchResponse::Batch( + reqs.iter() + .map(|req| req.execute_sync(root_node, context.clone())) + .collect(), + ), + } + } + + /// Executes a GraphQL request using the specified schema and context + /// + /// This is a simple wrapper around the `execute` function exposed in + /// GraphQLRequest + pub async fn execute<'a, QueryT, MutationT, SubscriptionT>( + &'a self, + root_node: &'a RootNode, + context: QueryT::Context, + ) -> GraphQLBatchResponse + where + S: Send + Sync, + QueryT: GraphQLTypeAsync + Clone + Sync>, + MutationT: GraphQLTypeAsync, + SubscriptionT: GraphQLSubscriptionType, + { + match self { + Self::Single(req) => { + let resp = req.execute(root_node, context).await; + GraphQLBatchResponse::Single(resp) + } + Self::Batch(reqs) => { + let resps = futures::future::join_all( + reqs.iter() + .map(|req| req.execute(root_node, context.clone())), + ) + .await; + GraphQLBatchResponse::Batch(resps) + } + } + } + + /// The operation names of the request. + pub fn operation_names(&self) -> Vec> { + match self { + Self::Single(req) => vec![req.operation_name.as_deref()], + Self::Batch(reqs) => reqs.iter().map(|r| r.operation_name.as_deref()).collect(), + } + } +} + /// Resolve a GraphQL subscription into `Value` using the /// specified schema and context. /// This is a wrapper around the `resolve_into_stream` function exposed at the top @@ -208,8 +297,8 @@ where where S: ser::Serializer, { - match self.0 { - Ok((ref res, ref err)) => { + match &self.0 { + Ok((res, err)) => { let mut map = serializer.serialize_map(None)?; map.serialize_key("data")?; @@ -222,7 +311,7 @@ where map.end() } - Err(ref err) => { + Err(err) => { let mut map = serializer.serialize_map(Some(1))?; map.serialize_key("errors")?; map.serialize_value(err)?; @@ -232,114 +321,6 @@ where } } -/// Simple wrapper around GraphQLRequest to allow the handling of Batch requests. -#[derive(Debug, Deserialize, PartialEq)] -#[serde(untagged)] -#[serde(bound = "InputValue: Deserialize<'de>")] -pub enum GraphQLBatchRequest -where - S: ScalarValue, -{ - /// A single operation request. - Single(GraphQLRequest), - - /// A batch operation request. - /// - /// Empty batch is considered as invalid value, so cannot be deserialized. - #[serde(deserialize_with = "deserialize_non_empty_batch")] - Batch(Vec>), -} - -fn deserialize_non_empty_batch<'de, D, T>(deserializer: D) -> Result, D::Error> -where - D: de::Deserializer<'de>, - T: Deserialize<'de>, -{ - use de::Error as _; - - let v = Vec::::deserialize(deserializer)?; - if v.is_empty() { - Err(D::Error::invalid_length( - 0, - &"non-empty batch of GraphQL requests", - )) - } else { - Ok(v) - } -} - -impl GraphQLBatchRequest -where - S: ScalarValue, -{ - /// Execute a GraphQL batch request synchronously using the specified schema and context - /// - /// This is a simple wrapper around the `execute_sync` function exposed in GraphQLRequest. - pub fn execute_sync<'a, QueryT, MutationT, SubscriptionT>( - &'a self, - root_node: &'a RootNode, - context: &QueryT::Context, - ) -> GraphQLBatchResponse - where - QueryT: GraphQLType, - MutationT: GraphQLType, - SubscriptionT: GraphQLType, - { - match *self { - Self::Single(ref req) => { - GraphQLBatchResponse::Single(req.execute_sync(root_node, context)) - } - Self::Batch(ref reqs) => GraphQLBatchResponse::Batch( - reqs.iter() - .map(|req| req.execute_sync(root_node, context)) - .collect(), - ), - } - } - - /// Executes a GraphQL request using the specified schema and context - /// - /// This is a simple wrapper around the `execute` function exposed in - /// GraphQLRequest - pub async fn execute<'a, QueryT, MutationT, SubscriptionT>( - &'a self, - root_node: &'a RootNode, - context: &'a QueryT::Context, - ) -> GraphQLBatchResponse - where - QueryT: GraphQLTypeAsync, - QueryT::TypeInfo: Sync, - QueryT::Context: Sync, - MutationT: GraphQLTypeAsync, - MutationT::TypeInfo: Sync, - SubscriptionT: GraphQLSubscriptionType, - SubscriptionT::TypeInfo: Sync, - S: Send + Sync, - { - match self { - Self::Single(req) => { - let resp = req.execute(root_node, context).await; - GraphQLBatchResponse::Single(resp) - } - Self::Batch(reqs) => { - let resps = futures::future::join_all( - reqs.iter().map(|req| req.execute(root_node, context)), - ) - .await; - GraphQLBatchResponse::Batch(resps) - } - } - } - - /// The operation names of the request. - pub fn operation_names(&self) -> Vec> { - match self { - Self::Single(req) => vec![req.operation_name.as_deref()], - Self::Batch(reqs) => reqs.iter().map(|r| r.operation_name.as_deref()).collect(), - } - } -} - /// Simple wrapper around the result (GraphQLResponse) from executing a GraphQLBatchRequest /// /// This struct implements Serialize, so you can simply serialize this diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index ff83576d2..5dfed3692 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -89,7 +89,7 @@ pub use crate::{ Selection, ToInputValue, Type, }, executor::{ - Applies, Context, ExecutionError, ExecutionResult, Executor, FieldError, FieldResult, + Applies, Context, Inject, ExecutionError, ExecutionResult, Executor, FieldError, FieldResult, FromContext, IntoFieldError, IntoResolvable, LookAheadArgument, LookAheadChildren, LookAheadList, LookAheadObject, LookAheadSelection, LookAheadValue, OwnedExecutor, Registry, ValuesStream, Variables, diff --git a/juniper/src/tests/fixtures/starwars/schema.rs b/juniper/src/tests/fixtures/starwars/schema.rs index f16940863..897e69f8f 100644 --- a/juniper/src/tests/fixtures/starwars/schema.rs +++ b/juniper/src/tests/fixtures/starwars/schema.rs @@ -9,7 +9,7 @@ use std::{collections::HashMap, pin::Pin}; -use crate::{Context, GraphQLEnum, graphql_interface, graphql_object, graphql_subscription}; +use crate::{Context, Inject, GraphQLEnum, graphql_interface, graphql_object, graphql_subscription}; #[derive(Clone, Copy, Debug)] pub struct Query; @@ -209,6 +209,8 @@ pub struct Database { impl Context for Database {} +impl Inject for Database {} + impl Database { pub fn new() -> Database { let mut humans = HashMap::new(); diff --git a/juniper/src/tests/subscriptions.rs b/juniper/src/tests/subscriptions.rs index 80a23b160..0437f6e94 100644 --- a/juniper/src/tests/subscriptions.rs +++ b/juniper/src/tests/subscriptions.rs @@ -94,7 +94,12 @@ fn create_and_execute( ), Vec>, > { - let request = GraphQLRequest::new(query, None, None); + let request = GraphQLRequest { + query, + operation_name: None, + variables: None, + extensions: None, + }; let root_node = Schema::new(MyQuery, EmptyMutation::new(), MySubscription); diff --git a/juniper_actix/Cargo.toml b/juniper_actix/Cargo.toml index 7f93673d6..f491f7a0c 100644 --- a/juniper_actix/Cargo.toml +++ b/juniper_actix/Cargo.toml @@ -23,8 +23,8 @@ rustdoc-args = ["--cfg", "docsrs"] [features] subscriptions = [ - "dep:actix-ws", - "dep:derive_more", + "dep:actix-ws", + "dep:derive_more", "dep:futures", "dep:juniper_graphql_ws", ] @@ -47,6 +47,7 @@ actix-test = "0.1" anyhow = "1.0.47" async-stream = "0.3" env_logger = "0.11" +futures = "0.3" juniper = { version = "0.17", path = "../juniper", features = ["expose-test-schema"] } log = "0.4" rand = "0.9" diff --git a/juniper_actix/examples/subscription.rs b/juniper_actix/examples/subscription.rs index 001aa70a2..868b07df1 100644 --- a/juniper_actix/examples/subscription.rs +++ b/juniper_actix/examples/subscription.rs @@ -6,17 +6,14 @@ use std::{pin::Pin, time::Duration}; use actix_cors::Cors; use actix_web::{ - App, Error, HttpRequest, HttpResponse, HttpServer, Responder, - http::header, - middleware, - web::{self, Data}, + App, Error, HttpRequest, HttpResponse, HttpServer, Responder, http::header, middleware, web, }; use juniper::{ EmptyMutation, FieldError, GraphQLObject, RootNode, graphql_subscription, graphql_value, tests::fixtures::starwars::schema::{Database, Query}, }; -use juniper_actix::{graphiql_handler, graphql_handler, playground_handler, subscriptions}; +use juniper_actix::{GraphQL, graphiql_handler, playground_handler, subscriptions}; use juniper_graphql_ws::ConnectionConfig; type Schema = RootNode, Subscription>; @@ -36,10 +33,9 @@ async fn graphiql() -> Result { async fn graphql( req: HttpRequest, payload: web::Payload, - schema: Data, + schema: web::Data, ) -> Result { - let context = Database::new(); - graphql_handler(&schema, &context, req, payload).await + GraphQL::handler(&schema, Database::new(), req, payload).await } async fn homepage() -> impl Responder { @@ -127,7 +123,7 @@ async fn main() -> std::io::Result<()> { HttpServer::new(move || { App::new() - .app_data(Data::new(schema())) + .app_data(web::Data::new(schema())) .wrap( Cors::default() .allow_any_origin() diff --git a/juniper_actix/src/lib.rs b/juniper_actix/src/lib.rs index 52527b80d..a2794a82e 100644 --- a/juniper_actix/src/lib.rs +++ b/juniper_actix/src/lib.rs @@ -3,18 +3,113 @@ #![cfg_attr(not(any(doc, test)), doc = env!("CARGO_PKG_NAME"))] #![cfg_attr(test, expect(unused_crate_dependencies, reason = "examples"))] +use std::marker::PhantomData; + use actix_web::{ Error, FromRequest, HttpMessage, HttpRequest, HttpResponse, error::JsonPayloadError, http::Method, web, }; use juniper::{ - ScalarValue, + Inject, DefaultScalarValue, GraphQLSubscriptionType, GraphQLTypeAsync, ScalarValue, http::{ GraphQLBatchRequest, GraphQLRequest, graphiql::graphiql_source, playground::playground_source, }, + Variables }; -use serde::Deserialize; +use serde::{Deserialize, de::DeserializeOwned}; + +pub type GraphQL = GraphQLWith; + +pub struct GraphQLWith>( + PhantomData<(S, RequestExtensions)>, +); + +impl GraphQLWith +where + S: ScalarValue + Send + Sync, + Extensions: DeserializeOwned, +{ + pub async fn handler( + schema: &juniper::RootNode, + context: Query::Context, + req: HttpRequest, + payload: web::Payload, + ) -> Result + where + Query: GraphQLTypeAsync + Clone + Sync>, + Mutation: GraphQLTypeAsync, + Subscription: GraphQLSubscriptionType, + { + match *req.method() { + Method::POST => Self::post_handler(schema, context, req, payload).await, + Method::GET => Self::get_handler(schema, context, req).await, + _ => Err(actix_web::error::UrlGenerationError::ResourceNotFound.into()), + } + } + + pub async fn get_handler( + schema: &juniper::RootNode, + context: Query::Context, + req: HttpRequest, + ) -> Result + where + Query: GraphQLTypeAsync + Sync>, + Mutation: GraphQLTypeAsync, + Subscription: GraphQLSubscriptionType, + { + let get_req = web::Query::::from_query(req.query_string())?; + let req = GraphQLRequest::::try_from(get_req.into_inner())?; + let gql_response = req.execute(schema, context).await; + let body_response = serde_json::to_string(&gql_response)?; + let mut response = match gql_response.is_ok() { + true => HttpResponse::Ok(), + false => HttpResponse::BadRequest(), + }; + Ok(response + .content_type("application/json") + .body(body_response)) + } + + pub async fn post_handler( + schema: &juniper::RootNode, + context: Query::Context, + req: HttpRequest, + payload: web::Payload, + ) -> Result + where + Query: GraphQLTypeAsync + Clone + Sync>, + Mutation: GraphQLTypeAsync, + Subscription: GraphQLSubscriptionType, + { + let req = match req.content_type() { + "application/json" => { + let body = String::from_request(&req, &mut payload.into_inner()).await?; + serde_json::from_str::>(&body) + .map_err(JsonPayloadError::Deserialize) + } + "application/graphql" => { + let body = String::from_request(&req, &mut payload.into_inner()).await?; + Ok(GraphQLBatchRequest::::Single( + GraphQLRequest { + query: body, + operation_name: None, + variables: None, + extensions: None, + }, + )) + } + _ => Err(JsonPayloadError::ContentType), + }?; + let gql_batch_response = req.execute(schema, context).await; + let gql_response = serde_json::to_string(&gql_batch_response)?; + let mut response = match gql_batch_response.is_ok() { + true => HttpResponse::Ok(), + false => HttpResponse::BadRequest(), + }; + Ok(response.content_type("application/json").body(gql_response)) + } +} #[derive(Deserialize, Clone, PartialEq, Debug)] #[serde(deny_unknown_fields)] @@ -23,126 +118,103 @@ struct GetGraphQLRequest { #[serde(rename = "operationName")] operation_name: Option, variables: Option, + #[serde(default)] + extensions: Option, } -impl From for GraphQLRequest +impl TryFrom for GraphQLRequest where S: ScalarValue, + Ext: DeserializeOwned, { - fn from(get_req: GetGraphQLRequest) -> Self { + type Error = JsonPayloadError; + + fn try_from(value: GetGraphQLRequest) -> Result { let GetGraphQLRequest { query, operation_name, variables, - } = get_req; - let variables = variables.map(|s| serde_json::from_str(&s).unwrap()); - Self::new(query, operation_name, variables) + extensions, + } = value; + Ok(Self { + query, + operation_name, + variables: variables + .as_deref() + .map(serde_json::from_str) + .transpose() + .map_err(JsonPayloadError::Deserialize)?, + extensions: extensions + .as_deref() + .map(serde_json::from_str) + .transpose() + .map_err(JsonPayloadError::Deserialize)?, + }) } } /// Actix Web GraphQL Handler for GET and POST requests -pub async fn graphql_handler( +pub async fn graphql_handler( schema: &juniper::RootNode, - context: &CtxT, + context: Query::Context, req: HttpRequest, - payload: actix_web::web::Payload, + payload: web::Payload, ) -> Result where - Query: juniper::GraphQLTypeAsync, - Query::TypeInfo: Sync, - Mutation: juniper::GraphQLTypeAsync, - Mutation::TypeInfo: Sync, - Subscription: juniper::GraphQLSubscriptionType, - Subscription::TypeInfo: Sync, - CtxT: Sync, + Query: GraphQLTypeAsync> + Clone + Sync>, + Mutation: GraphQLTypeAsync, + Subscription: GraphQLSubscriptionType, S: ScalarValue + Send + Sync, { - match *req.method() { - Method::POST => post_graphql_handler(schema, context, req, payload).await, - Method::GET => get_graphql_handler(schema, context, req).await, - _ => Err(actix_web::error::UrlGenerationError::ResourceNotFound.into()), - } + GraphQLWith::::handler(schema, context, req, payload).await } + /// Actix GraphQL Handler for GET requests -pub async fn get_graphql_handler( +pub async fn get_graphql_handler( schema: &juniper::RootNode, - context: &CtxT, + context: Query::Context, req: HttpRequest, ) -> Result where - Query: juniper::GraphQLTypeAsync, - Query::TypeInfo: Sync, - Mutation: juniper::GraphQLTypeAsync, - Mutation::TypeInfo: Sync, - Subscription: juniper::GraphQLSubscriptionType, - Subscription::TypeInfo: Sync, - CtxT: Sync, + Query: GraphQLTypeAsync> + Sync>, + Mutation: GraphQLTypeAsync, + Subscription: GraphQLSubscriptionType, S: ScalarValue + Send + Sync, { - let get_req = web::Query::::from_query(req.query_string())?; - let req = GraphQLRequest::from(get_req.into_inner()); - let gql_response = req.execute(schema, context).await; - let body_response = serde_json::to_string(&gql_response)?; - let mut response = match gql_response.is_ok() { - true => HttpResponse::Ok(), - false => HttpResponse::BadRequest(), - }; - Ok(response - .content_type("application/json") - .body(body_response)) + GraphQLWith::::get_handler(schema, context, req).await } /// Actix GraphQL Handler for POST requests -pub async fn post_graphql_handler( +pub async fn post_graphql_handler( schema: &juniper::RootNode, - context: &CtxT, + context: Query::Context, req: HttpRequest, - payload: actix_web::web::Payload, + payload: web::Payload, ) -> Result where - Query: juniper::GraphQLTypeAsync, - Query::TypeInfo: Sync, - Mutation: juniper::GraphQLTypeAsync, - Mutation::TypeInfo: Sync, - Subscription: juniper::GraphQLSubscriptionType, - Subscription::TypeInfo: Sync, - CtxT: Sync, + Query: GraphQLTypeAsync> + Clone + Sync>, + Mutation: GraphQLTypeAsync, + Subscription: GraphQLSubscriptionType, S: ScalarValue + Send + Sync, { - let req = match req.content_type() { - "application/json" => { - let body = String::from_request(&req, &mut payload.into_inner()).await?; - serde_json::from_str::>(&body) - .map_err(JsonPayloadError::Deserialize) - } - "application/graphql" => { - let body = String::from_request(&req, &mut payload.into_inner()).await?; - Ok(GraphQLBatchRequest::Single(GraphQLRequest::new( - body, None, None, - ))) - } - _ => Err(JsonPayloadError::ContentType), - }?; - let gql_batch_response = req.execute(schema, context).await; - let gql_response = serde_json::to_string(&gql_batch_response)?; - let mut response = match gql_batch_response.is_ok() { - true => HttpResponse::Ok(), - false => HttpResponse::BadRequest(), - }; - Ok(response.content_type("application/json").body(gql_response)) + GraphQLWith::::post_handler(schema, context, req, payload).await } -/// Create a handler that replies with an HTML page containing GraphiQL. This does not handle routing, so you can mount it on any endpoint +/// Creates a handler that replies with an HTML page containing [GraphiQL]. /// -/// For example: +/// This does not handle routing, so you can mount it on any endpoint. /// -/// ``` +/// # Example +/// +/// ```rust /// # use juniper_actix::graphiql_handler; /// # use actix_web::{web, App}; /// /// let app = App::new() -/// .route("/", web::get().to(|| graphiql_handler("/graphql", Some("/graphql/subscriptions")))); +/// .route("/", web::get().to(|| graphiql_handler("/graphql", Some("/graphql/subscriptions")))); /// ``` +/// +/// [GraphiQL]: https://github.com/graphql/graphiql pub async fn graphiql_handler( graphql_endpoint_url: &str, subscriptions_endpoint_url: Option<&'static str>, @@ -153,7 +225,11 @@ pub async fn graphiql_handler( .body(html)) } -/// Create a handler that replies with an HTML page containing GraphQL Playground. This does not handle routing, so you cant mount it on any endpoint. +/// Create a handler that replies with an HTML page containing [GraphQL Playground]. +/// +/// This does not handle routing, so you cant mount it on any endpoint. +/// +/// [GraphQL Playground]: https://github.com/prisma/graphql-playground pub async fn playground_handler( graphql_endpoint_url: &str, subscriptions_endpoint_url: Option<&'static str>, @@ -169,6 +245,7 @@ pub async fn playground_handler( pub mod subscriptions { use std::{pin::pin, sync::Arc}; + use crate::GraphQLWith; use actix_web::{ HttpRequest, HttpResponse, http::header::{HeaderName, HeaderValue}, @@ -177,7 +254,220 @@ pub mod subscriptions { use derive_more::with_trait::{Display, Error as StdError}; use futures::{SinkExt as _, StreamExt as _, future}; use juniper::{GraphQLSubscriptionType, GraphQLTypeAsync, RootNode, ScalarValue}; - use juniper_graphql_ws::{ArcSchema, Init, graphql_transport_ws, graphql_ws}; + use juniper_graphql_ws::{ArcSchema, Init, Schema, graphql_transport_ws, graphql_ws}; + + impl GraphQLWith + where + S: ScalarValue + Send + Sync + 'static, + { + /// Serves by auto-selecting between the + /// [legacy `graphql-ws` GraphQL over WebSocket Protocol][old] and the + /// [new `graphql-transport-ws` GraphQL over WebSocket Protocol][new], based on the + /// `Sec-Websocket-Protocol` HTTP header value. + /// + /// The `schema` argument is your [`juniper`] schema. + /// + /// The `init` argument is used to provide the custom [`juniper::Context`] and additional + /// configuration for connections. This can be a [`juniper_graphql_ws::ConnectionConfig`] if + /// the context and configuration are already known, or it can be a closure that gets + /// executed asynchronously whenever a client sends the subscription initialization message. + /// Using a closure allows to perform an authentication based on the parameters provided by + /// a client. + /// + /// [new]: https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md + /// [old]: https://github.com/apollographql/subscriptions-transport-ws/blob/v0.11.0/PROTOCOL.md + pub async fn auto_ws_handler( + req: HttpRequest, + stream: web::Payload, + schema: Arc>, + init: I, + ) -> Result + where + Query: GraphQLTypeAsync + + Send + + 'static, + Mutation: GraphQLTypeAsync + + Send + + 'static, + Subscription: GraphQLSubscriptionType + + Send + + 'static, + I: Init + Send, + { + if req + .headers() + .get("sec-websocket-protocol") + .map(AsRef::as_ref) + == Some("graphql-ws".as_bytes()) + { + graphql_ws_handler(req, stream, schema, init).await + } else { + graphql_transport_ws_handler(req, stream, schema, init).await + } + } + + /// Serves the [legacy `graphql-ws` GraphQL over WebSocket Protocol][old]. + /// + /// The `init` argument is used to provide the context and additional configuration for + /// connections. This can be a [`juniper_graphql_ws::ConnectionConfig`] if the context and + /// configuration are already known, or it can be a closure that gets executed + /// asynchronously when the client sends the `GQL_CONNECTION_INIT` message. Using a closure + /// allows to perform an authentication based on the parameters provided by a client. + /// + /// > __WARNING__: This protocol has been deprecated in favor of the + /// > [new `graphql-transport-ws` GraphQL over WebSocket Protocol][new], which + /// > is provided by the [`transport_ws_handler()`] method. + /// + /// [new]: https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md + /// [old]: https://github.com/apollographql/subscriptions-transport-ws/blob/v0.11.0/PROTOCOL.md + /// [`transport_ws_handler()`]: Self::transport_ws_handler + pub async fn ws_handler( + req: HttpRequest, + stream: web::Payload, + schema: Arc>, + init: I, + ) -> Result + where + Query: GraphQLTypeAsync + + Send + + 'static, + Mutation: GraphQLTypeAsync + + Send + + 'static, + Subscription: GraphQLSubscriptionType + + Send + + 'static, + I: Init + Send, + { + let (mut resp, mut ws_tx, ws_rx) = actix_ws::handle(&req, stream)?; + let (s_tx, mut s_rx) = graphql_ws::Connection::new(ArcSchema(schema), init).split(); + + actix_web::rt::spawn(async move { + let input = ws_rx + .map(|r| r.map(Message)) + .forward(s_tx.sink_map_err(|e| match e {})); + let output = pin!(async move { + while let Some(msg) = s_rx.next().await { + match serde_json::to_string(&msg) { + Ok(m) => { + if ws_tx.text(m).await.is_err() { + return; + } + } + Err(e) => { + _ = ws_tx + .close(Some(actix_ws::CloseReason { + code: actix_ws::CloseCode::Error, + description: Some(format!( + "error serializing response: {e}" + )), + })) + .await; + return; + } + } + } + _ = ws_tx + .close(Some((actix_ws::CloseCode::Normal, "Normal Closure").into())) + .await; + }); + + // No errors can be returned here, so ignoring is OK. + _ = future::select(input, output).await; + }); + + resp.headers_mut().insert( + HeaderName::from_static("sec-websocket-protocol"), + HeaderValue::from_static("graphql-ws"), + ); + Ok(resp) + } + + /// Serves the [new `graphql-transport-ws` GraphQL over WebSocket Protocol][new]. + /// + /// The `init` argument is used to provide the context and additional configuration for + /// connections. This can be a [`juniper_graphql_ws::ConnectionConfig`] if the context and + /// configuration are already known, or it can be a closure that gets executed + /// asynchronously when the client sends the `ConnectionInit` message. Using a closure + /// allows to perform an authentication based on the parameters provided by a client. + /// + /// [new]: https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md + pub async fn transport_ws_handler( + req: HttpRequest, + stream: web::Payload, + schema: Arc>, + init: I, + ) -> Result + where + Query: GraphQLTypeAsync + + Send + + 'static, + Mutation: GraphQLTypeAsync + + Send + + 'static, + Subscription: GraphQLSubscriptionType + + Send + + 'static, + I: Init + Send, + { + let (mut resp, mut ws_tx, ws_rx) = actix_ws::handle(&req, stream)?; + let (s_tx, mut s_rx) = + graphql_transport_ws::Connection::new(ArcSchema(schema), init).split(); + + actix_web::rt::spawn(async move { + let input = ws_rx + .map(|r| r.map(Message)) + .forward(s_tx.sink_map_err(|e| match e {})); + let output = pin!(async move { + while let Some(output) = s_rx.next().await { + match output { + graphql_transport_ws::Output::Message(msg) => { + match serde_json::to_string(&msg) { + Ok(m) => { + if ws_tx.text(m).await.is_err() { + return; + } + } + Err(e) => { + _ = ws_tx + .close(Some(actix_ws::CloseReason { + code: actix_ws::CloseCode::Error, + description: Some(format!( + "error serializing response: {e}", + )), + })) + .await; + return; + } + } + } + graphql_transport_ws::Output::Close { code, message } => { + _ = ws_tx + .close(Some(actix_ws::CloseReason { + code: code.into(), + description: Some(message), + })) + .await; + return; + } + } + } + _ = ws_tx + .close(Some((actix_ws::CloseCode::Normal, "Normal Closure").into())) + .await; + }); + + // No errors can be returned here, so ignoring is OK. + _ = future::select(input, output).await; + }); + + resp.headers_mut().insert( + HeaderName::from_static("sec-websocket-protocol"), + HeaderValue::from_static("graphql-transport-ws"), + ); + Ok(resp) + } + } /// Serves by auto-selecting between the /// [legacy `graphql-ws` GraphQL over WebSocket Protocol][old] and the @@ -194,33 +484,25 @@ pub mod subscriptions { /// /// [new]: https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md /// [old]: https://github.com/apollographql/subscriptions-transport-ws/blob/v0.11.0/PROTOCOL.md - pub async fn ws_handler( + pub async fn ws_handler( req: HttpRequest, stream: web::Payload, schema: Arc>, init: I, ) -> Result where - Query: GraphQLTypeAsync + Send + 'static, - Query::TypeInfo: Send + Sync, - Mutation: GraphQLTypeAsync + Send + 'static, - Mutation::TypeInfo: Send + Sync, - Subscription: GraphQLSubscriptionType + Send + 'static, - Subscription::TypeInfo: Send + Sync, - CtxT: Unpin + Send + Sync + 'static, + Query: GraphQLTypeAsync + + Send + + 'static, + Mutation: + GraphQLTypeAsync + Send + 'static, + Subscription: GraphQLSubscriptionType + + Send + + 'static, S: ScalarValue + Send + Sync + 'static, - I: Init + Send, + I: Init + Send, { - if req - .headers() - .get("sec-websocket-protocol") - .map(AsRef::as_ref) - == Some("graphql-ws".as_bytes()) - { - graphql_ws_handler(req, stream, schema, init).await - } else { - graphql_transport_ws_handler(req, stream, schema, init).await - } + GraphQLWith::::auto_ws_handler(req, stream, schema, init).await } /// Serves the [legacy `graphql-ws` GraphQL over WebSocket Protocol][old]. @@ -237,63 +519,25 @@ pub mod subscriptions { /// /// [new]: https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md /// [old]: https://github.com/apollographql/subscriptions-transport-ws/blob/v0.11.0/PROTOCOL.md - pub async fn graphql_ws_handler( + pub async fn graphql_ws_handler( req: HttpRequest, stream: web::Payload, schema: Arc>, init: I, ) -> Result where - Query: GraphQLTypeAsync + Send + 'static, - Query::TypeInfo: Send + Sync, - Mutation: GraphQLTypeAsync + Send + 'static, - Mutation::TypeInfo: Send + Sync, - Subscription: GraphQLSubscriptionType + Send + 'static, - Subscription::TypeInfo: Send + Sync, - CtxT: Unpin + Send + Sync + 'static, + Query: GraphQLTypeAsync + + Send + + 'static, + Mutation: + GraphQLTypeAsync + Send + 'static, + Subscription: GraphQLSubscriptionType + + Send + + 'static, S: ScalarValue + Send + Sync + 'static, - I: Init + Send, + I: Init + Send, { - let (mut resp, mut ws_tx, ws_rx) = actix_ws::handle(&req, stream)?; - let (s_tx, mut s_rx) = graphql_ws::Connection::new(ArcSchema(schema), init).split(); - - actix_web::rt::spawn(async move { - let input = ws_rx - .map(|r| r.map(Message)) - .forward(s_tx.sink_map_err(|e| match e {})); - let output = pin!(async move { - while let Some(msg) = s_rx.next().await { - match serde_json::to_string(&msg) { - Ok(m) => { - if ws_tx.text(m).await.is_err() { - return; - } - } - Err(e) => { - _ = ws_tx - .close(Some(actix_ws::CloseReason { - code: actix_ws::CloseCode::Error, - description: Some(format!("error serializing response: {e}")), - })) - .await; - return; - } - } - } - _ = ws_tx - .close(Some((actix_ws::CloseCode::Normal, "Normal Closure").into())) - .await; - }); - - // No errors can be returned here, so ignoring is OK. - _ = future::select(input, output).await; - }); - - resp.headers_mut().insert( - HeaderName::from_static("sec-websocket-protocol"), - HeaderValue::from_static("graphql-ws"), - ); - Ok(resp) + GraphQLWith::::ws_handler(req, stream, schema, init).await } /// Serves the [new `graphql-transport-ws` GraphQL over WebSocket Protocol][new]. @@ -305,79 +549,25 @@ pub mod subscriptions { /// authentication based on the parameters provided by a client. /// /// [new]: https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md - pub async fn graphql_transport_ws_handler( + pub async fn graphql_transport_ws_handler( req: HttpRequest, stream: web::Payload, schema: Arc>, init: I, ) -> Result where - Query: GraphQLTypeAsync + Send + 'static, - Query::TypeInfo: Send + Sync, - Mutation: GraphQLTypeAsync + Send + 'static, - Mutation::TypeInfo: Send + Sync, - Subscription: GraphQLSubscriptionType + Send + 'static, - Subscription::TypeInfo: Send + Sync, - CtxT: Unpin + Send + Sync + 'static, + Query: GraphQLTypeAsync + + Send + + 'static, + Mutation: + GraphQLTypeAsync + Send + 'static, + Subscription: GraphQLSubscriptionType + + Send + + 'static, S: ScalarValue + Send + Sync + 'static, - I: Init + Send, + I: Init + Send, { - let (mut resp, mut ws_tx, ws_rx) = actix_ws::handle(&req, stream)?; - let (s_tx, mut s_rx) = - graphql_transport_ws::Connection::new(ArcSchema(schema), init).split(); - - actix_web::rt::spawn(async move { - let input = ws_rx - .map(|r| r.map(Message)) - .forward(s_tx.sink_map_err(|e| match e {})); - let output = pin!(async move { - while let Some(output) = s_rx.next().await { - match output { - graphql_transport_ws::Output::Message(msg) => { - match serde_json::to_string(&msg) { - Ok(m) => { - if ws_tx.text(m).await.is_err() { - return; - } - } - Err(e) => { - _ = ws_tx - .close(Some(actix_ws::CloseReason { - code: actix_ws::CloseCode::Error, - description: Some(format!( - "error serializing response: {e}", - )), - })) - .await; - return; - } - } - } - graphql_transport_ws::Output::Close { code, message } => { - _ = ws_tx - .close(Some(actix_ws::CloseReason { - code: code.into(), - description: Some(message), - })) - .await; - return; - } - } - } - _ = ws_tx - .close(Some((actix_ws::CloseCode::Normal, "Normal Closure").into())) - .await; - }); - - // No errors can be returned here, so ignoring is OK. - _ = future::select(input, output).await; - }); - - resp.headers_mut().insert( - HeaderName::from_static("sec-websocket-protocol"), - HeaderValue::from_static("graphql-transport-ws"), - ); - Ok(resp) + GraphQLWith::::transport_ws_handler(req, stream, schema, init).await } #[derive(Debug)] @@ -436,21 +626,24 @@ mod tests { use actix_http::body::MessageBody; use actix_web::{ - App, + App, Error, HttpRequest, HttpResponse, dev::ServiceResponse, http, http::header::{ACCEPT, CONTENT_TYPE}, test::{self, TestRequest}, - web::Data, + web, }; use futures::future; use juniper::{ EmptyMutation, EmptySubscription, - http::tests::{HttpIntegration, TestResponse, run_http_test_suite}, + http::{ + GraphQLBatchRequest, + tests::{HttpIntegration, TestResponse, run_http_test_suite}, + }, tests::fixtures::starwars::schema::{Database, Query}, }; - use super::*; + use super::{GraphQL, graphiql_handler, playground_handler}; type Schema = juniper::RootNode, EmptySubscription>; @@ -468,11 +661,10 @@ mod tests { async fn index( req: HttpRequest, - payload: actix_web::web::Payload, + payload: web::Payload, schema: web::Data, ) -> Result { - let context = Database::new(); - graphql_handler(&schema, &context, req, payload).await + GraphQL::handler(&schema, Database::new(), req, payload).await } #[actix_web::rt::test] @@ -513,11 +705,11 @@ mod tests { assert_eq!(resp.status(), http::StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(), - "text/html; charset=utf-8" + "text/html; charset=utf-8", ); let body = take_response_body_string(resp).await; assert!(body.contains("const JUNIPER_URL = '/dogs-api/graphql';")); - assert!(body.contains("const JUNIPER_SUBSCRIPTIONS_URL = '/dogs-api/subscriptions';")) + assert!(body.contains("const JUNIPER_SUBSCRIPTIONS_URL = '/dogs-api/subscriptions';")); } #[actix_web::rt::test] @@ -578,7 +770,7 @@ mod tests { let mut app = test::init_service( App::new() - .app_data(Data::new(schema)) + .app_data(web::Data::new(schema)) .route("/", web::post().to(index)), ) .await; @@ -591,7 +783,7 @@ mod tests { ); assert_eq!( take_response_body_string(resp).await, - r#"{"data":{"hero":{"name":"R2-D2"}}}"# + r#"{"data":{"hero":{"name":"R2-D2"}}}"#, ); } @@ -610,7 +802,7 @@ mod tests { let mut app = test::init_service( App::new() - .app_data(Data::new(schema)) + .app_data(web::Data::new(schema)) .route("/", web::get().to(index)), ) .await; @@ -624,7 +816,7 @@ mod tests { ); assert_eq!( take_response_body_string(resp).await, - r#"{"data":{"hero":{"name":"R2-D2"}}}"# + r#"{"data":{"hero":{"name":"R2-D2"}}}"#, ); } @@ -654,7 +846,7 @@ mod tests { let mut app = test::init_service( App::new() - .app_data(Data::new(schema)) + .app_data(web::Data::new(schema)) .route("/", web::post().to(index)), ) .await; @@ -668,7 +860,7 @@ mod tests { ); assert_eq!( take_response_body_string(resp).await, - r#"[{"data":{"hero":{"name":"R2-D2"}}},{"data":{"hero":{"id":"1000","name":"Luke Skywalker"}}}]"# + r#"[{"data":{"hero":{"name":"R2-D2"}}},{"data":{"hero":{"id":"1000","name":"Luke Skywalker"}}}]"#, ); } @@ -693,7 +885,7 @@ mod tests { let mut app = test::init_service( App::new() - .app_data(Data::new(schema)) + .app_data(web::Data::new(schema)) .route("/", web::to(index)), ) .await; diff --git a/juniper_axum/src/extract.rs b/juniper_axum/src/extract.rs index 6ebf0af54..3780c4777 100644 --- a/juniper_axum/src/extract.rs +++ b/juniper_axum/src/extract.rs @@ -130,9 +130,12 @@ where String::from_request(req, state) .await .map(|body| { - Self(GraphQLBatchRequest::Single(GraphQLRequest::new( - body, None, None, - ))) + Self(GraphQLBatchRequest::Single(GraphQLRequest { + query: body, + operation_name: None, + variables: None, + extensions: None, + })) }) .map_err(|_| (StatusCode::BAD_REQUEST, "Not valid UTF-8 body").into_response()) } @@ -170,11 +173,12 @@ impl TryFrom for GraphQLRequest { operation_name, variables, } = req; - Ok(Self::new( + Ok(Self { query, operation_name, - variables.map(|v| serde_json::from_str(&v)).transpose()?, - )) + variables: variables.map(|v| serde_json::from_str(&v)).transpose()?, + extensions: None, + }) } } @@ -198,11 +202,12 @@ mod juniper_request_tests { .body(Body::empty()) .unwrap_or_else(|e| panic!("cannot build `Request`: {e}")); - let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest::new( - "{ add(a: 2, b: 3) }".into(), - None, - None, - ))); + let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest { + query: "{ add(a: 2, b: 3) }".into(), + operation_name: None, + variables: None, + extensions: None, + })); assert_eq!(do_from_request(req).await, expected); } @@ -219,11 +224,13 @@ mod juniper_request_tests { .body(Body::empty()) .unwrap_or_else(|e| panic!("cannot build `Request`: {e}")); - let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest::new( - "query($id: String!) { human(id: $id) { id, name, appearsIn, homePlanet } }".into(), - None, - Some(graphql_input_value!({"id": "1000"})), - ))); + let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest { + query: "query($id: String!) { human(id: $id) { id, name, appearsIn, homePlanet } }" + .into(), + operation_name: None, + variables: None, + extensions: None, + })); assert_eq!(do_from_request(req).await, expected); } @@ -235,11 +242,12 @@ mod juniper_request_tests { .body(Body::from(r#"{"query": "{ add(a: 2, b: 3) }"}"#)) .unwrap_or_else(|e| panic!("cannot build `Request`: {e}")); - let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest::new( - "{ add(a: 2, b: 3) }".to_string(), - None, - None, - ))); + let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest { + query: "{ add(a: 2, b: 3) }".to_string(), + operation_name: None, + variables: None, + extensions: None, + })); assert_eq!(do_from_request(req).await, expected); } @@ -251,11 +259,12 @@ mod juniper_request_tests { .body(Body::from(r#"{"query": "{ add(a: 2, b: 3) }"}"#)) .unwrap_or_else(|e| panic!("cannot build `Request`: {e}")); - let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest::new( - "{ add(a: 2, b: 3) }".to_string(), - None, - None, - ))); + let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest { + query: "{ add(a: 2, b: 3) }".into(), + operation_name: None, + variables: None, + extensions: None, + })); assert_eq!(do_from_request(req).await, expected); } @@ -267,11 +276,12 @@ mod juniper_request_tests { .body(Body::from(r#"{ add(a: 2, b: 3) }"#)) .unwrap_or_else(|e| panic!("cannot build `Request`: {e}")); - let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest::new( - "{ add(a: 2, b: 3) }".to_string(), - None, - None, - ))); + let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest { + query: "{ add(a: 2, b: 3) }".to_string(), + operation_name: None, + variables: None, + extensions: None, + })); assert_eq!(do_from_request(req).await, expected); } @@ -283,11 +293,12 @@ mod juniper_request_tests { .body(Body::from(r#"{ add(a: 2, b: 3) }"#)) .unwrap_or_else(|e| panic!("cannot build `Request`: {e}")); - let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest::new( - "{ add(a: 2, b: 3) }".to_string(), - None, - None, - ))); + let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest { + query: "{ add(a: 2, b: 3) }".to_string(), + operation_name: None, + variables: None, + extensions: None, + })); assert_eq!(do_from_request(req).await, expected); } diff --git a/juniper_hyper/src/lib.rs b/juniper_hyper/src/lib.rs index 41d408490..1337ac523 100644 --- a/juniper_hyper/src/lib.rs +++ b/juniper_hyper/src/lib.rs @@ -138,9 +138,12 @@ where let query = String::from_utf8(chunk.to_bytes().into()).map_err(GraphQLRequestError::BodyUtf8)?; - Ok(GraphQLBatchRequest::Single(GraphQLRequest::new( - query, None, None, - ))) + Ok(GraphQLBatchRequest::Single(GraphQLRequest { + query, + operation_name: None, + variables: None, + extensions: None, + })) } /// Generates a [`Response`] page containing [GraphiQL]. @@ -284,7 +287,12 @@ where } } match query { - Some(query) => Ok(JuniperGraphQLRequest::new(query, operation_name, variables)), + Some(query) => Ok(JuniperGraphQLRequest { + query, + operation_name, + variables, + extensions: None, + }), None => Err(GraphQLRequestError::Invalid( "'query' parameter is missing".into(), )), diff --git a/juniper_rocket/src/lib.rs b/juniper_rocket/src/lib.rs index bdff94bad..9705540a7 100644 --- a/juniper_rocket/src/lib.rs +++ b/juniper_rocket/src/lib.rs @@ -354,7 +354,12 @@ where match ctx.errors.is_empty() { true => Ok(GraphQLRequest(GraphQLBatchRequest::Single( - http::GraphQLRequest::new(ctx.query.unwrap(), ctx.operation_name, ctx.variables), + http::GraphQLRequest { + query: ctx.query.unwrap(), + operation_name: ctx.operation_name, + variables: ctx.variables, + extensions: None, + }, ))), false => Err(ctx.errors), } @@ -402,7 +407,12 @@ where Err(e) => return Outcome::Error((Status::BadRequest, e.to_string())), } } else { - GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None)) + GraphQLBatchRequest::Single(http::GraphQLRequest { + query: body, + operation_name: None, + variables: None, + extensions: None, + }) })) }) .await @@ -538,9 +548,12 @@ mod fromform_tests { assert!(result.is_ok()); let variables = ::serde_json::from_str::(r#"{"foo":"bar"}"#).unwrap(); - let expected = GraphQLRequest(http::GraphQLBatchRequest::Single( - http::GraphQLRequest::new("test".into(), None, Some(variables)), - )); + let expected = GraphQLRequest(http::GraphQLBatchRequest::Single(http::GraphQLRequest { + query: "test".into(), + operation_name: None, + variables: Some(variables), + extensions: None, + })); assert_eq!(result.unwrap(), expected); } @@ -551,9 +564,12 @@ mod fromform_tests { r#"query=test&variables={"foo":"x%20y%26%3F+z"}"#, )); let variables = ::serde_json::from_str::(r#"{"foo":"x y&? z"}"#).unwrap(); - let expected = GraphQLRequest(http::GraphQLBatchRequest::Single( - http::GraphQLRequest::new("test".into(), None, Some(variables)), - )); + let expected = GraphQLRequest(http::GraphQLBatchRequest::Single(http::GraphQLRequest { + query: "test".into(), + operation_name: None, + variables: Some(variables), + extensions: None, + })); assert_eq!(result.unwrap(), expected); } @@ -566,9 +582,12 @@ mod fromform_tests { assert!(result.is_ok()); - let expected = GraphQLRequest(http::GraphQLBatchRequest::Single( - http::GraphQLRequest::new("%foo bar baz&?".into(), Some("test".into()), None), - )); + let expected = GraphQLRequest(http::GraphQLBatchRequest::Single(http::GraphQLRequest { + query: "%foo bar baz&?".into(), + operation_name: "test".into(), + variables: None, + extensions: None, + })); assert_eq!(result.unwrap(), expected); } diff --git a/juniper_warp/src/lib.rs b/juniper_warp/src/lib.rs index 35e3e4b2c..0d10b9671 100644 --- a/juniper_warp/src/lib.rs +++ b/juniper_warp/src/lib.rs @@ -305,7 +305,12 @@ where .and_then(async |body: Bytes| { let query = str::from_utf8(body.as_ref()) .map_err(|e| reject::custom(FilterError::NonUtf8Body(e)))?; - let req = GraphQLRequest::new(query.into(), None, None); + let req = GraphQLRequest { + query: query.into(), + operation_name: None, + variables: None, + extensions: None, + }; Ok::, Rejection>(GraphQLBatchRequest::Single(req)) }) } @@ -319,15 +324,18 @@ where warp::get() .and(query::query()) .and_then(async |mut qry: HashMap| { - let req = GraphQLRequest::new( - qry.remove("query") + let req = GraphQLRequest { + query: qry + .remove("query") .ok_or_else(|| reject::custom(FilterError::MissingPathQuery))?, - qry.remove("operation_name"), - qry.remove("variables") + operation_name: qry.remove("operation_name"), + variables: qry + .remove("variables") .map(|vs| serde_json::from_str(&vs)) .transpose() .map_err(|e| reject::custom(FilterError::InvalidPathVariables(e)))?, - ); + extensions: todo!(), + }; Ok::, Rejection>(GraphQLBatchRequest::Single(req)) }) }