Upgrade GraphiQL to 3.0.5 version (#1188, #1069)

- track GraphiQL new version via @dependabot
- automate GraphiQL integration glue adapting for new versions
- rework `example/warp_subscriptions`  to support subscriptions in new GraphiQL
This commit is contained in:
Kai Ren 2023-09-13 17:34:44 +02:00 committed by GitHub
parent f172be5656
commit f9d90277bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 257 additions and 138 deletions

View file

@ -9,3 +9,8 @@ updates:
directory: /
schedule:
interval: daily
- package-ecosystem: npm
directory: /juniper/
schedule:
interval: daily

View file

@ -148,6 +148,23 @@ book.serve:
######################
# Forwarded commands #
######################
# Download and prepare actual version of GraphiQL static files, used for
# integrating it.
#
# Usage:
# make graphiql
graphiql:
@cd juniper/ && \
make graphiql
##################
# .PHONY section #
##################
@ -155,4 +172,5 @@ book.serve:
.PHONY: book fmt lint release test \
book.build book.serve \
cargo.fmt cargo.lint cargo.release cargo.test \
graphiql \
test.book test.cargo

View file

@ -11,6 +11,7 @@ env_logger = "0.10"
futures = "0.3"
juniper = { path = "../../juniper" }
juniper_graphql_ws = { path = "../../juniper_graphql_ws" }
juniper_graphql_transport_ws = { path = "../../juniper_graphql_transport_ws" }
juniper_warp = { path = "../../juniper_warp", features = ["subscriptions"] }
log = "0.4.8"
serde = { version = "1.0", features = ["derive"] }

View file

@ -7,8 +7,12 @@ use juniper::{
graphql_object, graphql_subscription, graphql_value, EmptyMutation, FieldError, GraphQLEnum,
RootNode,
};
use juniper_graphql_ws::ConnectionConfig;
use juniper_warp::{playground_filter, subscriptions::serve_graphql_ws};
use juniper_graphql_transport_ws::ConnectionConfig;
use juniper_graphql_ws::ConnectionConfig as LegacyConnectionConfig;
use juniper_warp::{
graphiql_filter, playground_filter,
subscriptions::{serve_graphql_transport_ws, serve_graphql_ws},
};
use warp::{http::Response, Filter};
#[derive(Clone)]
@ -108,13 +112,13 @@ struct Subscription;
#[graphql_subscription(context = Context)]
impl Subscription {
async fn users() -> UsersStream {
let mut counter = 0;
let mut interval = tokio::time::interval(Duration::from_secs(5));
let stream = async_stream::stream! {
counter += 1;
let mut counter = 0;
loop {
counter += 1;
interval.tick().await;
if counter == 2 {
if counter == 5 {
yield Err(FieldError::new(
"some field error from handler",
graphql_value!("some additional string"),
@ -156,36 +160,58 @@ async fn main() {
let qm_state = warp::any().map(|| Context);
let qm_graphql_filter = juniper_warp::make_graphql_filter(qm_schema, qm_state.boxed());
let root_node = Arc::new(schema());
let ws_schema = Arc::new(schema());
let transport_ws_schema = ws_schema.clone();
log::info!("Listening on 127.0.0.1:8080");
let routes = (warp::path("subscriptions")
let routes = warp::path("subscriptions")
.and(warp::ws())
.map(move |ws: warp::ws::Ws| {
let root_node = root_node.clone();
let transport_ws_schema = transport_ws_schema.clone();
ws.on_upgrade(move |websocket| async move {
serve_graphql_ws(websocket, root_node, ConnectionConfig::new(Context))
.map(|r| {
if let Err(e) = r {
println!("Websocket error: {e}");
}
})
.await
serve_graphql_transport_ws(
websocket,
transport_ws_schema,
ConnectionConfig::new(Context),
)
.map(|r| {
if let Err(e) = r {
println!("Websocket error: {e}");
}
})
.await
})
}))
.map(|reply| {
// TODO#584: remove this workaround
warp::reply::with_header(reply, "Sec-WebSocket-Protocol", "graphql-ws")
})
.or(warp::post()
.and(warp::path("graphql"))
.and(qm_graphql_filter))
.or(warp::get()
.and(warp::path("playground"))
.and(playground_filter("/graphql", Some("/subscriptions"))))
.or(homepage)
.with(log);
})
.or(warp::path("legacy-subscriptions")
.and(warp::ws())
.map(move |ws: warp::ws::Ws| {
let ws_schema = ws_schema.clone();
ws.on_upgrade(move |websocket| async move {
serve_graphql_ws(websocket, ws_schema, LegacyConnectionConfig::new(Context))
.map(|r| {
if let Err(e) = r {
println!("Websocket error: {e}");
}
})
.await
})
})
.map(|reply| {
// TODO#584: remove this workaround
warp::reply::with_header(reply, "Sec-WebSocket-Protocol", "graphql-ws")
}))
.or(warp::post()
.and(warp::path("graphql"))
.and(qm_graphql_filter))
.or(warp::get()
.and(warp::path("playground"))
.and(playground_filter("/graphql", Some("/legacy-subscriptions"))))
.or(warp::get()
.and(warp::path("graphiql"))
.and(graphiql_filter("/graphql", Some("/subscriptions"))))
.or(homepage)
.with(log);
warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
}

3
juniper/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/node_modules/
/package-lock.json
/yarn.lock

View file

@ -51,6 +51,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
- Disabled `chrono` [Cargo feature] by default.
- Removed `scalar-naivetime` [Cargo feature].
- Removed lifetime parameter from `ParseError`, `GraphlQLError`, `GraphQLBatchRequest` and `GraphQLRequest`. ([#1081], [#528])
- Upgraded [GraphiQL] to 3.0.5 version (requires new [`graphql-ws` GraphQL over WebSocket Protocol] integration on server, see `examples/warp_subscriptions`). ([#1188])
### Added
@ -121,6 +122,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
[#1145]: /../../pull/1145
[#1147]: /../../pull/1147
[#1176]: /../../pull/1176
[#1188]: /../../pull/1188
[ba1ed85b]: /../../commit/ba1ed85b3c3dd77fbae7baf6bc4e693321a94083
[CVE-2022-31173]: /../../security/advisories/GHSA-4rx6-g5vg-5f3j
@ -140,6 +142,8 @@ See [old CHANGELOG](/../../blob/juniper-v0.15.9/juniper/CHANGELOG.md).
[`chrono-tz` crate]: https://docs.rs/chrono-tz
[`time` crate]: https://docs.rs/time
[Cargo feature]: https://doc.rust-lang.org/cargo/reference/features.html
[`graphql-ws` GraphQL over WebSocket Protocol]: https://github.com/graphql/graphiql
[GraphiQL]: https://github.com/enisdenjo/graphql-ws/master/PROTOCOL.md
[graphql-scalars.dev]: https://graphql-scalars.dev
[October 2021]: https://spec.graphql.org/October2021
[object safety]: https://doc.rust-lang.org/reference/items/traits.html#object-safety

View file

@ -18,7 +18,7 @@ repository = "https://github.com/graphql-rust/juniper"
readme = "README.md"
categories = ["asynchronous", "web-programming", "web-programming::http-server"]
keywords = ["apollo", "graphql", "server", "web"]
exclude = ["/release.toml"]
include = ["/src/", "/CHANGELOG.md", "/LICENSE", "/README.md"]
[package.metadata.docs.rs]
all-features = true

56
juniper/Makefile Normal file
View file

@ -0,0 +1,56 @@
###############################
# Common defaults/definitions #
###############################
# Checks two given strings for equality.
eq = $(if $(or $(1),$(2)),$(and $(findstring $(1),$(2)),\
$(findstring $(2),$(1))),1)
# Multiplatform prefix of `sed -i` commands.
sed-i = sed -i$(if $(call eq,$(shell uname -s),Darwin), '',)
######################
# Project parameters #
######################
GRAPHIQL_VER ?= $(strip \
$(shell grep -m1 '"graphiql": "' package.json | cut -d '"' -f4))
############
# Commands #
############
# Download and prepare actual version of GraphiQL static files, used for
# integrating it.
#
# Usage:
# make graphiql
graphiql:
curl -fL -o src/http/graphiql.html \
https://raw.githubusercontent.com/graphql/graphiql/graphiql%40$(GRAPHIQL_VER)/examples/graphiql-cdn/index.html
$(sed-i) 's|https://unpkg.com/graphiql/|https://unpkg.com/graphiql@$(GRAPHIQL_VER)/|g' \
src/http/graphiql.html
$(sed-i) "s|'https://swapi-graphql.netlify.app/.netlify/functions/index'|GRAPHQL_URL|g" \
src/http/graphiql.html
$(sed-i) "s|url: GRAPHQL_URL,|url: GRAPHQL_URL,\n subscriptionUrl: normalizeSubscriptionEndpoint(GRAPHQL_URL, GRAPHQL_SUBSCRIPTIONS_URL)|" \
src/http/graphiql.html
$(sed-i) 's|<script>|<script>\n<!-- inject -->|' \
src/http/graphiql.html
$(sed-i) '/X-Example-Header/d' \
src/http/graphiql.html
##################
# .PHONY section #
##################
.PHONY: graphiql

9
juniper/package.json Normal file
View file

@ -0,0 +1,9 @@
{
"private": true,
"scripts": {
"postinstall": "make graphiql"
},
"dependencies": {
"graphiql": "3.0.5"
}
}

View file

@ -0,0 +1,75 @@
<!--
* Copyright (c) 2021 GraphQL Contributors
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
-->
<!doctype html>
<html lang="en">
<head>
<title>GraphiQL</title>
<style>
body {
height: 100%;
margin: 0;
width: 100%;
overflow: hidden;
}
#graphiql {
height: 100vh;
}
</style>
<!--
This GraphiQL example depends on Promise and fetch, which are available in
modern browsers, but can be "polyfilled" for older browsers.
GraphiQL itself depends on React DOM.
If you do not want to rely on a CDN, you can host these files locally or
include them directly in your favored resource bundler.
-->
<script
crossorigin
src="https://unpkg.com/react@18/umd/react.development.js"
></script>
<script
crossorigin
src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
></script>
<script
src="https://unpkg.com/graphiql@3.0.5/graphiql.min.js"
type="application/javascript"
></script>
<script
src="https://unpkg.com/@graphiql/plugin-explorer/dist/index.umd.js"
crossorigin
></script>
<!--
These two files can be found in the npm module, however you may wish to
copy them directly into your environment, or perhaps include them in your
favored resource bundler.
-->
<link rel="stylesheet" href="https://unpkg.com/graphiql@3.0.5/graphiql.min.css" />
</head>
<body>
<div id="graphiql">Loading...</div>
<script>
<!-- inject -->
const root = ReactDOM.createRoot(document.getElementById('graphiql'));
const fetcher = GraphiQL.createFetcher({
url: GRAPHQL_URL,
subscriptionUrl: normalizeSubscriptionEndpoint(GRAPHQL_URL, GRAPHQL_SUBSCRIPTIONS_URL)
});
const explorerPlugin = GraphiQLPluginExplorer.explorerPlugin();
root.render(
React.createElement(GraphiQL, {
fetcher,
defaultEditorToolsVisibility: true,
plugins: [explorerPlugin],
}),
);
</script>
</body>
</html>

View file

@ -0,0 +1,14 @@
function normalizeSubscriptionEndpoint(endpoint, subscriptionEndpoint) {
if (subscriptionEndpoint) {
if (subscriptionEndpoint.startsWith('/')) {
const secure =
endpoint.includes('https') || location.href.includes('https')
? 's'
: ''
return `ws${secure}://${location.host}${subscriptionEndpoint}`
} else {
return subscriptionEndpoint.replace(/^http/, 'ws')
}
}
return null
}

View file

@ -6,7 +6,7 @@
///
/// ```
/// # use juniper::http::graphiql::graphiql_source;
/// let graphiql = graphiql_source("/graphql", Some("ws://localhost:8080/subscriptions"));
/// let graphiql = graphiql_source("/graphql", Some("/subscriptions"));
/// ```
pub fn graphiql_source(
graphql_endpoint_url: &str,
@ -18,110 +18,20 @@ pub fn graphiql_source(
""
};
let stylesheet_source = r#"
<style>
body {
height: 100%;
margin: 0;
width: 100%;
overflow: hidden;
}
include_str!("graphiql.html").replace(
"<!-- inject -->",
// Language: JavaScript
&format!(
"
var GRAPHQL_URL = '{graphql_url}';
var GRAPHQL_SUBSCRIPTIONS_URL = '{graphql_subscriptions_url}';
#graphiql {
height: 100vh;
}
</style>
"#;
let fetcher_source = r#"
<script>
if (usingSubscriptions) {
var subscriptionEndpoint = normalizeSubscriptionEndpoint(GRAPHQL_URL, GRAPHQL_SUBSCRIPTIONS_URL);
var subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(subscriptionEndpoint, { reconnect: true });
}
{grahiql_js}
function normalizeSubscriptionEndpoint(endpoint, subscriptionEndpoint) {
if (subscriptionEndpoint) {
if (subscriptionEndpoint.startsWith('/')) {
const secure =
endpoint.includes('https') || location.href.includes('https')
? 's'
: ''
return `ws${secure}://${location.host}${subscriptionEndpoint}`
} else {
return subscriptionEndpoint.replace(/^http/, 'ws')
}
}
return null
}
function graphQLFetcher(graphQLParams, opts) {
const { headers = {} } = opts;
return fetch(
GRAPHQL_URL,
{
method: 'post',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...headers,
},
body: JSON.stringify(graphQLParams),
credentials: 'omit',
},
).then(function (response) {
return response.json().catch(function () {
return response.text();
});
});
}
var fetcher = usingSubscriptions ? window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, graphQLFetcher) : graphQLFetcher;
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher,
defaultVariableEditorOpen: true,
}),
document.getElementById('graphiql'),
);
</script>
"#;
format!(
r#"
<!DOCTYPE html>
<html>
<head>
<title>GraphQL</title>
{stylesheet_source}
<script
crossorigin
src="https://unpkg.com/react@17/umd/react.development.js"
></script>
<script
crossorigin
src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"
></script>
<link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />
</head>
<body>
<div id="graphiql">Loading...</div>
<script
src="https://unpkg.com/graphiql/graphiql.min.js"
type="application/javascript"
></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,
graphql_subscriptions_url = subscriptions_endpoint,
using_subscriptions = subscriptions_endpoint_url.is_some(),
",
graphql_url = graphql_endpoint_url,
graphql_subscriptions_url = subscriptions_endpoint,
grahiql_js = include_str!("graphiql.js"),
),
)
}

View file

@ -515,10 +515,8 @@ mod tests {
"text/html; charset=utf-8"
);
let body = take_response_body_string(resp).await;
assert!(body.contains("<script>var GRAPHQL_URL = '/dogs-api/graphql';</script>"));
assert!(body.contains(
"<script>var GRAPHQL_SUBSCRIPTIONS_URL = '/dogs-api/subscriptions';</script>"
))
assert!(body.contains("var GRAPHQL_URL = '/dogs-api/graphql';"));
assert!(body.contains("var GRAPHQL_SUBSCRIPTIONS_URL = '/dogs-api/subscriptions';"))
}
#[actix_web::rt::test]

View file

@ -556,7 +556,7 @@ mod tests {
);
let body = String::from_utf8(response.body().to_vec()).unwrap();
assert!(body.contains("<script>var GRAPHQL_URL = '/dogs-api/graphql';</script>"));
assert!(body.contains("var GRAPHQL_URL = '/dogs-api/graphql';"));
}
#[tokio::test]