Add support for parse request from query string #1085

This commit is contained in:
Sunli 2022-09-25 12:10:52 +08:00
parent 73f6b0b136
commit 053c1178c2
11 changed files with 93 additions and 9 deletions

View File

@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# [4.0.14] 2022-09-25
# Add support for parse request from query string. [#1085](https://github.com/async-graphql/async-graphql/issues/1085)
# [4.0.13] 2022-09-09
- Compare to expected schema [#1048](https://github.com/async-graphql/async-graphql/pull/1048)

View File

@ -54,6 +54,7 @@ static_assertions = "1.1.0"
tempfile = "3.2.0"
thiserror = "1.0.24"
base64 = "0.13.0"
serde_urlencoded = "0.7.0"
# Feature optional dependencies
bson = { version = "2.4.0", optional = true, features = [

View File

@ -23,7 +23,6 @@ futures-channel = "0.3.13"
futures-util = { version = "0.3.0", default-features = false }
serde_cbor = { version = "0.11.2", optional = true }
serde_json = "1.0.64"
serde_urlencoded = "0.7.0"
thiserror = "1.0.30"
[features]

View File

@ -75,7 +75,8 @@ impl FromRequest for GraphQLBatchRequest {
.unwrap_or_default();
if req.method() == Method::GET {
let res = serde_urlencoded::from_str(req.query_string());
let res = async_graphql::http::parse_query_string(req.query_string())
.map_err(|err| io::Error::new(ErrorKind::Other, err));
Box::pin(async move { Ok(Self(async_graphql::BatchRequest::Single(res?))) })
} else if req.method() == Method::POST {
let content_type = req

View File

@ -20,6 +20,5 @@ bytes = "1.0.1"
futures-util = "0.3.0"
http-body = "0.4.2"
serde_json = "1.0.66"
serde_urlencoded = "0.7.0"
tokio-util = { version = "0.7.1", features = ["io", "compat"] }
tower-service = "0.3"

View File

@ -107,7 +107,7 @@ where
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
if let (&Method::GET, uri) = (req.method(), req.uri()) {
let res = serde_urlencoded::from_str(uri.query().unwrap_or_default()).map_err(|err| {
let res = async_graphql::http::parse_query_string(uri.query().unwrap_or_default()).map_err(|err| {
ParseRequestError::Io(std::io::Error::new(
ErrorKind::Other,
format!("failed to parse graphql request from uri query: {}", err),

View File

@ -3,7 +3,6 @@ use poem::{
async_trait,
error::BadRequest,
http::{header, Method},
web::Query,
FromRequest, Request, RequestBody, Result,
};
use tokio_util::compat::TokioAsyncReadCompatExt;
@ -68,7 +67,9 @@ pub struct GraphQLBatchRequest(pub async_graphql::BatchRequest);
impl<'a> FromRequest<'a> for GraphQLBatchRequest {
async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result<Self> {
if req.method() == Method::GET {
let req = Query::from_request(req, body).await?.0;
let req =
async_graphql::http::parse_query_string(req.uri().query().unwrap_or_default())
.map_err(BadRequest)?;
Ok(Self(async_graphql::BatchRequest::Single(req)))
} else {
let content_type = req

View File

@ -132,7 +132,9 @@ pub async fn receive_batch_request_opts<State: Clone + Send + Sync + 'static>(
opts: MultipartOptions,
) -> tide::Result<async_graphql::BatchRequest> {
if request.method() == Method::Get {
request.query::<async_graphql::Request>().map(Into::into)
async_graphql::http::parse_query_string(request.url().query().unwrap_or_default())
.map(Into::into)
.map_err(|err| tide::Error::new(StatusCode::BadRequest, err))
} else if request.method() == Method::Post {
let body = request.take_body();
let content_type = request

View File

@ -35,7 +35,13 @@ where
Subscription: SubscriptionType + 'static,
{
warp::any()
.and(warp::get().and(warp::query()).map(BatchRequest::Single))
.and(warp::get().and(warp::filters::query::raw()).and_then(
|query_string: String| async move {
async_graphql::http::parse_query_string(&query_string)
.map(Into::into)
.map_err(|e| warp::reject::custom(GraphQLBadRequest(e)))
},
))
.or(warp::post()
.and(warp::header::optional::<String>("content-type"))
.and(warp::body::stream())

View File

@ -6,18 +6,59 @@ mod multipart;
mod playground_source;
mod websocket;
use std::io::ErrorKind;
use futures_util::io::{AsyncRead, AsyncReadExt};
pub use graphiql_source::graphiql_source;
pub use graphiql_v2_source::GraphiQLSource;
use mime;
pub use multipart::MultipartOptions;
pub use playground_source::{playground_source, GraphQLPlaygroundConfig};
use serde::Deserialize;
pub use websocket::{
ClientMessage, Protocols as WebSocketProtocols, WebSocket, WsMessage, ALL_WEBSOCKET_PROTOCOLS,
};
use crate::{BatchRequest, ParseRequestError, Request};
/// Parse a GraphQL request from a query string.
pub fn parse_query_string(input: &str) -> Result<Request, ParseRequestError> {
#[derive(Deserialize)]
struct RequestSerde {
#[serde(default)]
pub query: String,
pub operation_name: Option<String>,
pub variables: Option<String>,
pub extensions: Option<String>,
}
let request: RequestSerde = serde_urlencoded::from_str(input)
.map_err(|err| std::io::Error::new(ErrorKind::Other, err))?;
let variables = request
.variables
.map(|data| serde_json::from_str(&data))
.transpose()
.map_err(|err| {
std::io::Error::new(ErrorKind::Other, format!("invalid variables: {}", err))
})?
.unwrap_or_default();
let extensions = request
.extensions
.map(|data| serde_json::from_str(&data))
.transpose()
.map_err(|err| {
std::io::Error::new(ErrorKind::Other, format!("invalid extensions: {}", err))
})?
.unwrap_or_default();
Ok(Request {
operation_name: request.operation_name,
variables,
extensions,
..Request::new(request.query)
})
}
/// Receive a GraphQL request from a content type and body.
pub async fn receive_body(
content_type: Option<impl AsRef<str>>,
@ -116,3 +157,33 @@ pub async fn receive_batch_cbor(body: impl AsyncRead) -> Result<BatchRequest, Pa
serde_cbor::from_slice::<BatchRequest>(&data)
.map_err(|e| ParseRequestError::InvalidRequest(Box::new(e)))
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
use crate::{value, Variables};
#[test]
fn test_parse_query_string() {
let request = parse_query_string("variables=%7B%7D&extensions=%7B%22persistedQuery%22%3A%7B%22sha256Hash%22%3A%22cde5de0a350a19c59f8ddcd9646e5f260b2a7d5649ff6be8e63e9462934542c3%22%2C%22version%22%3A1%7D%7D").unwrap();
assert_eq!(request.query.as_str(), "");
assert_eq!(request.variables, Variables::default());
assert_eq!(request.extensions, {
let mut extensions = HashMap::new();
extensions.insert("persistedQuery".to_string(), value!({
"sha256Hash": "cde5de0a350a19c59f8ddcd9646e5f260b2a7d5649ff6be8e63e9462934542c3",
"version": 1,
}));
extensions
});
let request = parse_query_string("query={a}&variables=%7B%22a%22%3A10%7D").unwrap();
assert_eq!(request.query.as_str(), "{a}");
assert_eq!(
request.variables,
Variables::from_value(value!({ "a" : 10 }))
);
}
}

View File

@ -9,7 +9,7 @@ use serde::{Deserialize, Deserializer, Serialize};
use crate::{ConstValue, Name};
/// Variables of a query.
#[derive(Debug, Clone, Default, Serialize)]
#[derive(Debug, Clone, Default, Serialize, Eq, PartialEq)]
#[serde(transparent)]
pub struct Variables(BTreeMap<Name, ConstValue>);