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:
parent
f0ccc2e35e
commit
47f7ffaa5b
15 changed files with 117 additions and 19 deletions
|
@ -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`
|
||||
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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>")]
|
||||
|
|
|
@ -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,
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue