Add subscriptions support for GraphiQL (#619)

* Add subscriptions support on GraphiQL

Addresses #501

BREAKING CHANGE: `juniper::http::graphiql::graphiql_source` now requires
a second parameter

BREAKING CHANGE: `juniper_hyper::graphiql` now requires
a second parameter

BREAKING CHANGE: `juniper_iron::GraphiQLHandler::new` now requires
a second parameter

BREAKING CHANGE: `juniper_rocket::graphiql_source` now requires
a second parameter

BREAKING CHANGE: `juniper_warp::graphiql_filter` now requires
a second parameter

* Add test where graphiql subscriptions endpoint is not None
This commit is contained in:
Matthew Kuo 2020-04-12 20:03:09 -05:00 committed by GitHub
parent f0ccc2e35e
commit 47f7ffaa5b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 117 additions and 19 deletions

View file

@ -18,11 +18,14 @@ See [#419](https://github.com/graphql-rust/juniper/pull/419).
- `SchemaType` is now public
- This is helpful when using `context.getSchema()` inside of your field resolvers
- Support subscriptions in GraphiQL
See [#569](https://github.com/graphql-rust/juniper/pull/569).
## Breaking Changes
- `juniper::graphiql` has moved to `juniper::http::graphiql`
- `juniper::http::graphiql::graphiql_source` now requies a second parameter for subscriptions
- remove old `graphql_object!` macro, rename `object` proc macro to `graphql_object`

View file

@ -1,7 +1,23 @@
//! Utility module to generate a GraphiQL interface
/// Generate the HTML source to show a GraphiQL interface
pub fn graphiql_source(graphql_endpoint_url: &str) -> String {
///
/// The subscriptions endpoint URL can optionally be provided. For example:
///
/// ```
/// # use juniper::http::graphiql::graphiql_source;
/// let graphiql = graphiql_source("/graphql", Some("ws://localhost:8080/subscriptions"));
/// ```
pub fn graphiql_source(
graphql_endpoint_url: &str,
subscriptions_endpoint_url: Option<&str>,
) -> String {
let subscriptions_endpoint = if let Some(sub_url) = subscriptions_endpoint_url {
sub_url
} else {
""
};
let stylesheet_source = r#"
<style>
html, body, #app {
@ -14,6 +30,10 @@ pub fn graphiql_source(graphql_endpoint_url: &str) -> String {
"#;
let fetcher_source = r#"
<script>
if (usingSubscriptions) {
var subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(GRAPHQL_SUBSCRIPTIONS_URL, { reconnect: true });
}
function graphQLFetcher(params) {
return fetch(GRAPHQL_URL, {
method: 'post',
@ -33,9 +53,12 @@ pub fn graphiql_source(graphql_endpoint_url: &str) -> String {
}
});
}
var fetcher = usingSubscriptions ? window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, graphQLFetcher) : graphQLFetcher;
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: graphQLFetcher,
fetcher,
}),
document.querySelector('#app'));
</script>
@ -53,16 +76,22 @@ pub fn graphiql_source(graphql_endpoint_url: &str) -> String {
<body>
<div id="app"></div>
<script src="//cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.js"></script>
<script src="//unpkg.com/subscriptions-transport-ws@0.8.3/browser/client.js"></script>
<script src="//unpkg.com/graphiql-subscriptions-fetcher@0.0.2/browser/client.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/graphiql@0.17.5/graphiql.min.js"></script>
<script>var GRAPHQL_URL = '{graphql_url}';</script>
<script>var usingSubscriptions = {using_subscriptions};</script>
<script>var GRAPHQL_SUBSCRIPTIONS_URL = '{graphql_subscriptions_url}';</script>
{fetcher_source}
</body>
</html>
"#,
graphql_url = graphql_endpoint_url,
stylesheet_source = stylesheet_source,
fetcher_source = fetcher_source
fetcher_source = fetcher_source,
graphql_subscriptions_url = subscriptions_endpoint,
using_subscriptions = subscriptions_endpoint_url.is_some(),
)
}

View file

@ -2,6 +2,10 @@
- Compatibility with the latest `juniper`.
## Breaking Changes
- `juniper_hyper::graphiql` now requires a second parameter for subscriptions
# [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_hyper-0.5.2)
- Compatibility with the latest `juniper`.

View file

@ -31,7 +31,7 @@ async fn main() {
let ctx = ctx.clone();
async move {
match (req.method(), req.uri().path()) {
(&Method::GET, "/") => juniper_hyper::graphiql("/graphql").await,
(&Method::GET, "/") => juniper_hyper::graphiql("/graphql", None).await,
(&Method::GET, "/graphql") | (&Method::POST, "/graphql") => {
juniper_hyper::graphql(root_node, ctx, req).await
}

View file

@ -114,10 +114,16 @@ async fn parse_post_req<S: ScalarValue>(
.map_err(GraphQLRequestError::BodyJSONError)
}
pub async fn graphiql(graphql_endpoint: &str) -> Result<Response<Body>, hyper::Error> {
pub async fn graphiql(
graphql_endpoint: &str,
subscriptions_endpoint: Option<&str>,
) -> Result<Response<Body>, hyper::Error> {
let mut resp = new_html_response(StatusCode::OK);
// XXX: is the call to graphiql_source blocking?
*resp.body_mut() = Body::from(juniper::http::graphiql::graphiql_source(graphql_endpoint));
*resp.body_mut() = Body::from(juniper::http::graphiql::graphiql_source(
graphql_endpoint,
subscriptions_endpoint,
));
Ok(resp)
}

View file

@ -2,6 +2,10 @@
- Compatibility with the latest `juniper`.
## Breaking Changes
- `juniper_iron::GraphiQLHandler::new` now requires a second parameter for subscriptions
# [[0.6.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_iron-0.6.2)
- Compatibility with the latest `juniper`.

View file

@ -29,7 +29,7 @@ fn main() {
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
let graphiql_endpoint = GraphiQLHandler::new("/graphql");
let graphiql_endpoint = GraphiQLHandler::new("/graphql", None);
mount.mount("/", graphiql_endpoint);
mount.mount("/graphql", graphql_endpoint);

View file

@ -159,6 +159,7 @@ pub struct GraphQLHandler<
/// Handler that renders `GraphiQL` - a graphical query editor interface
pub struct GraphiQLHandler {
graphql_url: String,
subscription_url: Option<String>,
}
/// Handler that renders `GraphQL Playground` - a graphical query editor interface
@ -275,9 +276,10 @@ impl GraphiQLHandler {
///
/// The provided URL should point to the URL of the attached `GraphQLHandler`. It can be
/// relative, so a common value could be `"/graphql"`.
pub fn new(graphql_url: &str) -> GraphiQLHandler {
pub fn new(graphql_url: &str, subscription_url: Option<&str>) -> GraphiQLHandler {
GraphiQLHandler {
graphql_url: graphql_url.to_owned(),
subscription_url: subscription_url.map(|s| s.to_owned()),
}
}
}
@ -326,7 +328,10 @@ impl Handler for GraphiQLHandler {
Ok(Response::with((
content_type,
status::Ok,
juniper::http::graphiql::graphiql_source(&self.graphql_url),
juniper::http::graphiql::graphiql_source(
&self.graphql_url,
self.subscription_url.as_deref(),
),
)))
}
}

View file

@ -3,6 +3,10 @@
- Compatibility with the latest `juniper`.
- Rocket integration does not require default features.
## Breaking Changes
- `juniper_rocket::graphiql_source` now requires a second parameter for subscriptions
# [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.5.2)
- Compatibility with the latest `juniper`.

View file

@ -11,7 +11,7 @@ type Schema = RootNode<'static, Query, EmptyMutation<Database>, EmptySubscriptio
#[rocket::get("/")]
fn graphiql() -> content::Html<String> {
juniper_rocket::graphiql_source("/graphql")
juniper_rocket::graphiql_source("/graphql", None)
}
#[rocket::get("/graphql?<request>")]

View file

@ -71,9 +71,13 @@ where
pub struct GraphQLResponse(pub Status, pub String);
/// Generate an HTML page containing GraphiQL
pub fn graphiql_source(graphql_endpoint_url: &str) -> content::Html<String> {
pub fn graphiql_source(
graphql_endpoint_url: &str,
subscriptions_endpoint: Option<&str>,
) -> content::Html<String> {
content::Html(juniper::http::graphiql::graphiql_source(
graphql_endpoint_url,
subscriptions_endpoint,
))
}

View file

@ -74,6 +74,7 @@ pub struct GraphQLResponse(pub Status, pub String);
pub fn graphiql_source(graphql_endpoint_url: &str) -> content::Html<String> {
content::Html(juniper::http::graphiql::graphiql_source(
graphql_endpoint_url,
None,
))
}

View file

@ -9,6 +9,7 @@ to `juniper` to be reused in other http integrations, since this implementation
- Update `playground_filter` to support subscription endpoint URLs
- Update `warp` to 0.2
- Rename synchronous `execute` to `execute_sync`, add asynchronous `execute`
- `juniper_warp::graphiql_filter` now requires a second parameter for subscriptions
# [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_warp-0.5.2)

View file

@ -41,7 +41,7 @@ async fn main() {
warp::serve(
warp::get()
.and(warp::path("graphiql"))
.and(juniper_warp::graphiql_filter("/graphql"))
.and(juniper_warp::graphiql_filter("/graphql", None))
.or(homepage)
.or(warp::path("graphql").and(graphql_filter))
.with(log),

View file

@ -299,20 +299,41 @@ type Response = Pin<
/// # use warp::Filter;
/// # use juniper_warp::graphiql_filter;
/// #
/// let graphiql_route = warp::path("graphiql").and(graphiql_filter("/graphql"));
/// let graphiql_route = warp::path("graphiql").and(graphiql_filter("/graphql",
/// None));
/// ```
///
/// Or with subscriptions support, provide the subscriptions endpoint URL:
///
/// ```
/// # extern crate warp;
/// # extern crate juniper_warp;
/// #
/// # use warp::Filter;
/// # use juniper_warp::graphiql_filter;
/// #
/// let graphiql_route = warp::path("graphiql").and(graphiql_filter("/graphql",
/// Some("ws://localhost:8080/subscriptions")));
/// ```
pub fn graphiql_filter(
graphql_endpoint_url: &'static str,
subscriptions_endpoint: Option<&'static str>,
) -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> {
warp::any()
.map(move || graphiql_response(graphql_endpoint_url))
.map(move || graphiql_response(graphql_endpoint_url, subscriptions_endpoint))
.boxed()
}
fn graphiql_response(graphql_endpoint_url: &'static str) -> warp::http::Response<Vec<u8>> {
fn graphiql_response(
graphql_endpoint_url: &'static str,
subscriptions_endpoint: Option<&'static str>,
) -> warp::http::Response<Vec<u8>> {
warp::http::Response::builder()
.header("content-type", "text/html;charset=utf-8")
.body(juniper::http::graphiql::graphiql_source(graphql_endpoint_url).into_bytes())
.body(
juniper::http::graphiql::graphiql_source(graphql_endpoint_url, subscriptions_endpoint)
.into_bytes(),
)
.expect("response is valid")
}
@ -568,14 +589,14 @@ mod tests {
#[test]
fn graphiql_response_does_not_panic() {
graphiql_response("/abcd");
graphiql_response("/abcd", None);
}
#[tokio::test]
async fn graphiql_endpoint_matches() {
let filter = warp::get()
.and(warp::path("graphiql"))
.and(graphiql_filter("/graphql"));
.and(graphiql_filter("/graphql", None));
let result = request()
.method("GET")
.path("/graphiql")
@ -591,7 +612,7 @@ mod tests {
let filter = warp::get()
.and(warp::path("dogs-api"))
.and(warp::path("graphiql"))
.and(graphiql_filter("/dogs-api/graphql"));
.and(graphiql_filter("/dogs-api/graphql", None));
let response = request()
.method("GET")
.path("/dogs-api/graphiql")
@ -609,6 +630,22 @@ mod tests {
assert!(body.contains("<script>var GRAPHQL_URL = '/dogs-api/graphql';</script>"));
}
#[tokio::test]
async fn graphiql_endpoint_with_subscription_matches() {
let filter = warp::get().and(warp::path("graphiql")).and(graphiql_filter(
"/graphql",
Some("ws:://localhost:8080/subscriptions"),
));
let result = request()
.method("GET")
.path("/graphiql")
.header("accept", "text/html")
.filter(&filter)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn playground_endpoint_matches() {
let filter = warp::get()